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}の製品です")
}
データクラスが出来上がりました。このクラスには
- プライマリコンストラクタで初期化する4つのvalプロパティ
- クラス内部で初期化するvarプロパティ
- メソッド
が含まれています。誤解があるかもしれませんが、データクラスにもメソッドを定義することは可能です。
なおデータクラスの定義ではプライマリコンストラクタ引数が必須となります。
ではインスタンス化して使ってみましょう。
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
}
コード真中付近で使用しているhashCode
とequals
では、インスタンスが違うものであっても、プロパティの値が同じであれば「2つは等値である」という判定が下されます。ちなみにequals
は==
演算子を使っても同じ意味になります。
通常のクラスとの違い
このうちtoString
、hashCode
、equals
はどのクラスでも利用可能です。なぜならこれらのメソッドは、すべてのクラスのスーパークラスである「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つのメソッドがオーバーライドされます。