🐥

【Kotlin】data class と class

2021/05/16に公開

はじめに

Kotlin のdata classclassの理解の違いで予期せぬバグに遭遇しました。
勉強になったので動作を確認しながら学んだ内容を残します!

「data class」と「class」比較表

data class class
equals() 同じ値(プロパティ)を持ったインスタンスかどうかを比較する 同じインスタンスであるかどうかを比較する
hashCode() インスタンスの値(プロパティ)によってコードが出力される インスタンスごとに固有のコードが出力される
toString() 「型(プロパティ=値)」の形式で返す 「クラス@xxxxx」の形式?で返す
componentN() インスタンスのプロパティに簡単にアクセスが可能 なし
copy() 同じプロパティを持つ(一部変更も可能)別のインスタンスを生成する なし

以下でそれぞれ動作を確認していく。
※ 実行環境: Kotlin Playground

equals()

class: 同じインスタンスであればtrueそうでなければfalse
data class: 同じ値(プロパティ)を持ったインスタンスであればtrueそうでなければfalse

紛らわしいぜ...

コード

class Person(var name: String, var age: Int)
data class DataPerson(var name: String, var age: Int)

fun main() {
    val person1 = Person("タナカ", 20)
    val person2 = Person("タナカ", 20)
    
    val dataPerson1 = DataPerson("タナカ", 20)
    val dataPerson2 = DataPerson("タナカ", 20)
    
    println("classの同じインスタンス: " + person1.equals(person1))
    println("classの別インスタンス:  " + person1.equals(person2))
    
    println("data classの同じインスタンス: " + dataPerson1.equals(dataPerson1))
    println("data classの別インスタンス:  " + dataPerson1.equals(dataPerson2))
}

出力結果

classの同じインスタンス: true
classの別インスタンス:  false

data classの同じインスタンス: true
data classの別インスタンス:  true

equals() 参考資料

hashCode()

class: インスタンスが異なれば、値(プロパティ)は同じでも異なるハッシュコードが出力される
data class: 同じ値(プロパティ)を持ったインスタンスであれば、異なるインスタンスであっても同じハッシュコードが出力される

コード

class Person(var name: String, var age: Int)
data class DataPerson(var name: String, var age: Int)

fun main() {
    val person1 = Person("タナカ", 20)
    val person2 = Person("タナカ", 20)
    val person3 = Person("スズキ", 21)
   
    val dataPerson1 = DataPerson("タナカ", 20)
    val dataPerson2 = DataPerson("タナカ", 20)
    val dataPerson3 = DataPerson("スズキ", 21)
    
    println("class person1: " + person1.hashCode())
    println("class person2:  " + person2.hashCode())
    println("class person3:  " + person3.hashCode())
    
    println("data class dataPerson1: " + dataPerson1.hashCode())
    println("data class dataPerson2:  " + dataPerson2.hashCode())
    println("data class dataPerson3:  " + dataPerson3.hashCode())
}

出力結果

class person1: 1414644648
class person2:  1995265320
class person3:  746292446

data class dataPerson1: 384151028
data class dataPerson2:  384151028
data class dataPerson3:  383956969

toString()

コード

class Person(var name: String, var age: Int)
data class DataPerson(var name: String, var age: Int)

fun main() {
    val person1 = Person("タナカ", 20)
    
    val dataPerson1 = DataPerson("タナカ", 20)

    println("class person1: " + person1.toString())
    
    println("data class dataPerson1: " + dataPerson1.toString())
}

出力結果

class person1: Person@5451c3a8
data class dataPerson1: DataPerson(name=タナカ, age=20)

componentN()

コンストラクタ引数N番目のプロパティにアクセスできる。

コード

data class DataPerson(var name: String, var age: Int, var from: String)

fun main() {
    val dataPerson1 = DataPerson("タナカ", 20, "日本")
    val dataPerson2 = DataPerson("スズキ", 25, "アメリカ")
    
    println("dataPerson1: " + "①" + dataPerson1.component1() + " ②" + dataPerson1.component2() + " ③" + dataPerson1.component3())
    println("dataPerson2: " + "①" + dataPerson2.component1() + " ②" + dataPerson2.component2() + " ③" + dataPerson2.component3())
}

出力結果

dataPerson1: ①タナカ ②20 ③日本
dataPerson2: ①スズキ ②25 ③アメリカ

分解宣言

こんなこともできるらしい。
(JavaScript の分割代入みたい。)

コード

data class DataPerson(var name: String, var age: Int, var from: String)

fun main() {
    val dataPerson1 = DataPerson("タナカ", 20, "日本")
    val dataPerson2 = DataPerson("スズキ", 25, "アメリカ")
    
    val (name, age, from) = dataPerson1
    // 変数名は何でもOK	   
    val (a, b, c) = dataPerson2
    
    println("dataPerson1: " + "①" + name + " ②" + age + " ③" + from)
    println("dataPerson2: " + "①" + a + " ②" + b + " ③" + c)
}

出力結果

dataPerson1: ①タナカ ②20 ③日本
dataPerson2: ①スズキ ②25 ③アメリカ

この分解宣言は以下のコードにコンパイルされているようである。

val name = dataPerson1.dataPerson1()
val age = dataPerson1.component2()
val from = dataPerson1.dataPerson3()

copy()

同じプロパティを持つ別のインスタンスを生成することができる。
(または一部の異なるプロパティを持つ別のインスタンスを生成)

コード

data class DataPerson(var name: String, var age: Int, var from: String)

fun main() {
    val original = DataPerson("タナカ", 20, "日本")
    val copy = original.copy()
    
    // $ は 「+ original.toString()」 と同じ
    println("original: $original")
    println("copy    : $copy")
}

出力結果

original: DataPerson(name=タナカ, age=20, from=日本)
copy    : DataPerson(name=タナカ, age=20, from=日本)

一部のプロパティを変えてコピー

コード

data class DataPerson(var name: String, var age: Int, var from: String)

fun main() {
    val original = DataPerson("タナカ", 20, "日本")
    val copy = original.copy(name = "スズキ", from = "アメリカ")
    
    println("original: $original")
    println("copy    : $copy")
}

出力結果

original: DataPerson(name=タナカ, age=20, from=日本)
copy    : DataPerson(name=スズキ, age=20, from=アメリカ)

※ 代入するだけではコピーされない

コード

data class DataPerson(var name: String, var age: Int, var from: String)

fun main() {
    val original = DataPerson("タナカ", 20, "日本")
    val dainyu = original
    val copy = original.copy()
    
    original.name = "スズキ"
    
    // original の name に代入しただけなのに、dainyu の name も"スズキ"に変わってしまっている!!
    println("original: $original")
    println("dainyu  : $dainyu")
    println("copy    : $copy")
}

出力結果

original: DataPerson(name=スズキ, age=20, from=日本)
dainyu  : DataPerson(name=スズキ, age=20, from=日本)
copy    : DataPerson(name=タナカ, age=20, from=日本)

これは代入するだけでは、同じオブジェクトを参照してしまうためである。
つまり、今回で言うとoriginaldainyuも同じものを見にいっている。

最後に

記事投稿エラい。
疲れました..

大変参考にさせていただきました!ありがとうございました!

Discussion