[Kotlin]データクラス – そのメリットと注意点

Kotlinには様々な種類のクラスが存在します。その中でも理解しやすいのが、値を格納するために利用する「データクラス」です。

今回はデータクラスを作りながら、その使い方とメリットについてお話していきます。

データクラスの作り方

まずはデータクラスを作ってみます。通常のクラスを作ったことがあるなら、ごく簡単に作成できるはずです。

//「data class」で始め、プライマリコンストラクタが必須
data class Phone(val product: String,
                 val manufacturer: String,
                 val price: Int,

    //初期値は通常通り設定可能
                 val weight: Int? = null){

    //クラス内部で初期化されるプロパティ
    var discription = "これは携帯のクラスです"

    //メソッドを含めることも可能
    fun printStatus() = println("${product}は${manufacturer}の製品です")
}

データクラスが出来上がりました。このクラスには

  1. プライマリコンストラクタで初期化する4つのvalプロパティ
  2. クラス内部で初期化するvarプロパティ
  3. メソッド

が含まれています。誤解があるかもしれませんが、データクラスにもメソッドを定義することは可能です。

なおデータクラスの定義ではプライマリコンストラクタ引数が必須となります。

ではインスタンス化して使ってみましょう。

fun main() {
    val iPhone = Phone("iPhone11","apple",74800,194)
    val pixel = Phone("Pixel4","google",89980,162)

    println(iPhone.weight)     //194
    println(pixel.manufacturer)   //google
    iPhone.printStatus()       //iPhone11はappleの製品です
}

データクラスのメリット

普通に使えますが、これじゃただのクラスと差はありません。わざわざデータクラスとして定義するメリットは何でしょうか?
データクラスを定義するメリットとは、端的に言えば以下のような「便利に扱えるメソッドが自動的に実装される」ことです。

メソッド名機能
toString型名とプロパティ名、値を文字列として返す
hashCodeプロパティの値に基づくハッシュ値を返す
equalsオブジェクトの同値性を調べ、Booleanを返す
copyオブジェクトのコピーを返す
componentN (Nは整数)N番目のプロパティの値を返す

それぞれのメソッドを利用して、挙動を確認しましょう。

fun main() {
    val iPhone = Phone("iPhone11","apple",74800,194)
    val otherPhone = Phone("iPhone11","apple",74800,194)
    val pixel = Phone("Pixel4","google",89980,162)

    //copyメソッドで同じプロパティを持つオブジェクトを作成
    val pixelCopy = pixel.copy()

    println(pixel.toString())
    //Phone(product=Pixel4, manufacturer=google, price=89980, weight=162)

    println(iPhone.hashCode())       //-367304217
    println(otherPhone.hashCode())   //-367304217

    println(otherPhone.equals(iPhone))   //true

    //componentNメソッド。1始まりという点に注意
    println(pixelCopy.component2())    //google
}

コード真中付近で使用しているhashCodeequalsでは、インスタンスが違うものであっても、プロパティの値が同じであれば「2つは等値である」という判定が下されます。ちなみにequals==演算子を使っても同じ意味になります。

通常のクラスとの違い

このうちtoStringhashCodeequalsはどのクラスでも利用可能です。なぜならこれらのメソッドは、すべてのクラスのスーパークラスである「Anyクラス」で既に実装されているメソッドだからです。

ではデータクラスの定義文から頭の「data」を外し、通常のクラスとして定義してから、同じことをもう一度試してみましょう。

fun main() {
    val iPhone = Phone("iPhone11","apple",74800,194)
    val otherPhone = Phone("iPhone11","apple",74800,194)
    val pixel = Phone("Pixel4","google",89980,162)

    println(pixel.toString())     //Phone@27bc2616

    println(iPhone.hashCode())    //960604060
    println(pixel.hashCode())      //1349393271

    println(otherPhone.equals(iPhone))    //false
}

結果は随分違ったものになります。Anyクラスで実装されている、各メソッドのデフォルトの機能がこちらです。

メソッド名機能
toString「型名@ハッシュ値(16進数)」を返す
hashCodeインスタンス固有のハッシュ値を返す
equalsオブジェクトの同一性を調べ、Booleanを返す

通常のクラスでは同じプロパティ値を持つオブジェクトでも、それぞれの参照先が異なるためにハッシュ値と同一性チェックは異なるものになります。はたしてどちらの方が使いやすいでしょうか?

通常のクラスではこれらのメソッドをオーバーライドするのが一般的ですが、データクラスではほぼ不要です。

データクラスの便利な利用法

他にもデータクラスの便利な利用法をいくつかご紹介しておきましょう。

分解宣言

分解宣言とは「複数の変数への代入」を一気に行う手法です。Listなどのコンテナ型では割とメジャーな代入方法ですが、データクラスは分解宣言によって、オブジェクトが持つプロパティ値を変数に代入することができます。

val (pro, man, pr, we) = pixel
println("${pro}は${man}の製品。価格は${pr}円で重量は${we}g")

//Pixel4はgoogleの製品。価格は89980円で重量は162g

copyメソッドでプロパティを変更する

copyは同値の別インスタンスを生成できますが、その際プロパティの値を変更することができます。
「プロパティの一部だけ値を変更して、新たなオブジェクトを作る」といった場合はcopyを使うと、全てのプロパティを入力せずに済みます。

val pixel = Phone("Pixel4","google",89980,162)

//productプロパティのみ変更したオブジェクトを作成
val changedPixel = pixel.copy(product = "Pixel3XL")

println(changedPixel.manufacturer)
//google

データクラスの注意点

これまで見てきたように、データクラスはインスタンスの同値性を調べるのに便利ですが、いくつかの注意点があります。

これらを把握した上で、より便利にデータクラスを使っていきましょう。

プライマリコンストラクタで初期化した値のみ比較する

equalsの比較には、プライマリコンストラクタで初期化した値しか材料として使用されません。

fun main() {
    val iPhone = Phone("iPhone11","apple", 74800, 194)
    val otherPhone = Phone("iPhone11","apple", 74800, 194)

    //クラス内部で初期化されたプロパティを変更
    otherPhone.discription = "OK"

    println(iPhone.equals(otherPhone))    //true
}

変数otherPhoneは、6行目でdiscriptionプロパティを書き換えています。これはvarとしてクラス内部で初期化されたプロパティです。
この時点で厳密に言えば、otherPhoneとiPhoneが持つ値は全く同じではありません。しかし同値性チェックはtrueです。

この現象はデータクラスのequalsがdiscriptionプロパティの値を考慮していないために起こります。同じようにtoStringも、クラス内部で初期化されたプロパティの値は出力しません。

プロパティとして配列を利用する場合

プロパティにJavaの配列を含む場合、equalsメソッドはオーバーライドすることが推奨されます。これは一体どういうことでしょうか?

data class example(val ary: Array<Int>)

このデータクラスのオブジェクトを2つ作成して比較してみましょう。

val a = Example(arrayOf(1,2,3))
val b = Example(arrayOf(1,2,3))
println(a == b)    //false

変数aとbはプロパティの値が全く同じであるにも関わらず、equalsによるチェックはfalseです。配列の比較はJVMでは同一性のチェックになってしまうので気を付けてください。

IntelliJで簡単!メソッドをオーバーライドする方法

IntelliJでは配列をプロパティとして使用するデータクラスを宣言した場合、メソッドのオーバーライドを促す通知が現れます。

オーバーライドする場合はここでAlt+Shift+Enterを押します。

equalsで比較する時、どのプロパティを比較材料とするかの選択です。よければ「次へ」をクリック。

同じようにhashCodeをどのプロパティに基づいて生成するかの選択です。「完了」をクリックすると、

2つのメソッドがオーバーライドされます。

タイトルとURLをコピーしました