前回エントリで データを保持するクラスの toString, equals, hashCode のオーバーライドについて簡単に書きました。
これらの実装は、Intellij IDEA などのIDEを使う事で簡単に自動生成することが可能です。
Kotlin では、データクラスを利用することで、これらをわざわざコード上に書く必要すらありません。
前回記述した以下クラス Person については、
class Person(val name: String, val age: Int) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Person if (name != other.name) return false if (age != other.age) return false return true } override fun hashCode(): Int { var result = name.hashCode() result = 31 * result + age return result } override fun toString(): String { return "Person(name='$name', age=$age)" } }
以下のように記述可能です
data class Person(val name: String, val age: Int)
data
という修飾子を付けるだけです。非常に簡単!
これによって、以下のようにJavaの標準的なメソッドをオーバライドしたクラスを作成したのと同じ状態になります。
- インスタンス同士を比較するための
equals
- HashMapなどのハッシュベースのコンテナのキーに利用される
hashCode
- クラスの文字列表現を生成する(フィールドの各内容を出力する)ための
toString
equals と hashCode は、プライマリコンストラクタで宣言された全てのプロパティが考慮されます。
equals は全てのプロパティの値が等しい事をチェックします。
hashCode は全てのプロパティのハッシュ値に依存した値を返します。
各メソッドの動きを確認してみました
val person1 = Person("KotlinTaro", 25) val person2 = Person("KotlinTaro", 25) val person3 = Person("KotlinTaro", 26) // 文字列表現の確認 println(person1) // 等価性の確認 println("person1 == person2 -> ${person1 == person2}") println("person1 == person3 -> ${person1 == person3}") // ハッシュコードの確認 println("person1 hashCode -> ${person1.hashCode()}") println("person2 hashCode -> ${person2.hashCode()}") println("person3 hashCode -> ${person3.hashCode()}")
結果は以下の通りです
Person(name=KotlinTaro, age=25) person1 == person2 -> true person1 == person3 -> false person1 hashCode -> -3502258 person2 hashCode -> -3502258 person3 hashCode -> -3502257
不変性の推奨
データクラスのプロパティは val で宣言が必須ではなく、var での宣言可能です。
しかし、読み取り専用のプロパティのみを利用して、データクラスのインスタンスがイミュータブル(immutable)となることが強く推奨されているようです。
よく知られた事実ではありますが、イミュータブルなオブジェクトはマルチスレッド環境下での強さがあります。
一度生成されたオブジェクトは、変更される事がないため複数スレッドからは参照する操作しかできず、特定のスレッドが変更をしてしまい
copy メソッド
データクラスには、さらに copy というメソッドが実装されています。
データクラスをイミュータブルとして利用することを容易にしてくれます。
copy メドッドはクラスのプロパティの値を変更しながらインスタンスをコピーしてくれます。
ちなみにプロパティの値を変更なしでのコピーも可能です。(コピーコンストラクタのようなイメージに近いと思います)
サンプルコード と実行結果は以下の通りです
プロパティの変更なしでのコピー
◾️サンプルコード
val person1 = Person("KotlinTaro", 25) // 値の変更なしでコピー val person2 = person1.copy() println("person1 == person2 -> ${person1 == person2}") // 等価 println("person1 === person2 -> ${person1 === person2}") // 参照は別物 println(person2)
◾️実行結果
person1 == person2 -> true person1 === person2 -> false Person(name=KotlinTaro, age=25)
単純にコピーしたので、等価なインスタンスですが参照は異なっています
プロパティの値を変更しながらのコピー
◾️サンプルコード
val person1 = Person("KotlinTaro", 25) // 値を変更しながらコピー val person2 = person1.copy(age = 40) println("person1 == person2 -> ${person1 == person2}") println("person1 === person2 -> ${person1 === person2}") println(person2)
◾️実行結果
person1 == person2 -> false person1 === person2 -> false Person(name=KotlinTaro, age=40)
プロパティを変更しながらコピーしたので、インスタンスは等価でもなく参照も異なっています
まとめ
data
修飾子の指定により、ボイラープレートコード無しで有用な データクラス を宣言することができることが分かりました。