覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - データクラス

前回エントリで データを保持するクラスの 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 修飾子の指定により、ボイラープレートコード無しで有用な データクラス を宣言することができることが分かりました。


関連エントリ