Kotlin

はじめてのKotlin:データクラスの関数~equals()/hashCode()/copy()~

More than 1 year has passed since last update.

データクラスの操作

 Kotlin(コトリン)のデータクラスの宣言は通常はたったの1行です。このたった1行で実はデフォルトの操作用メソッドが裏で実装されており、それを利用することができます。
 ここでは、コトリンのデータクラスにデフォルト実装される関数を用いたデータクラスの操作について紹介します。

データクラスで利用できるデフォルト関数

 コトリンのデータクラスはコンパイルされると以下の関数をデフォルトで利用することできるようになります。

デフォルト関数一覧

関数名 関数の機能
equals() オブジェクトが等しいかを調べる(hashCode()を利用)
hashCode() ハッシュコードを得る
copy() オブジェクトをコピーする
toString() 文字列に変換する
componentN() N番目のプロパティの内容を取り出す

データクラスの デフォルト関数を使うための条件

 データクラスでデフォルトで提供される関数を使うためには以下の条件を満たす必要があります。

  • データクラスが抽象クラス(abstract)でないこと。
  • プライマリコンストラクタが少なくともひとつの引数を持つこと。
  • プライマリコンストラクタの引数は必ず val または var キーワードで宣言されていること

 abstract キーワードで修飾されたデータクラスは実態がないのでデフォルト関数を利用できません。デフォルト関数を利用するためにはオブジェクトが中身の実態、すなわちプロパティを持っている必要があります。中身がないデータクラスはデフォルト関数を利用できません。
 プロパティを持つためにはプライマリコンストラクタが引数を持っていないといけません。
 以下のデータクラスはデフォルト関数が利用できます。

  • open キーワードで修飾された、どこからでも利用可能なオープンなデータクラス
  • sealed キーワードで修飾された、同一ソースファイル内のみで利用可能なデータクラス
  • inner キーワードで修飾された、他のクラス内などで定義されたデータクラス

equals() および hashCode() 関数

 equals() 関数はデータクラスのオブジェクトが等しいかどうかを調べます。等しいときには true を、等しくないときには false を返します。
 hashCode() 関数はデータクラスのオブジェクトのハッシュコードを生成して返します。
 equals() 関数はこのハッシュコードの値を比較して、データクラスのオブジェクトが等しいかどうかを判断します。

equals() および hashCode() 関数のコード例

 equals() および hashCode() 関数のコード例と実行結果を以下に示します。

equals()およびhashCode()関数のコード例
data class 都市( val 名称: String, val 人口: Int )

fun main( args: Array<String> ) {
  val 新潟市 = 都市( "新潟市", 797176 )
  val cityOfNiigata = 都市( "新潟市", 797176 )

  println( 新潟市.hashCode() )
  println( cityOfNiigata.hashCode() )

  println( 新潟市.equals( cityOfNiigata ) )

  println(
    都市( "静岡市", 699255 ).equals( 都市( "浜松市", 807130 ) )
  )
}

ideone :https://ideone.com/fork/IVXFOE
Kotlin都市.PNG

実行結果
804461605
804461605
true
false

 コード例では、コンストラクタ引数にStringクラス型の 名称 とIntクラス型の 人口 をもつデータクラス 都市 を定義しています。

val 新潟市 = 都市( "新潟市", 797176 )
val cityOfNiigata = 都市( "新潟市", 797176 )

 main()関数内でこのデータクラス 都市 を2回インスタンス化してオブジェクトを生成し、不変変数 新潟市cityOfNiigata にそれぞれ代入しています。

println( 新潟市.hashCode() )
println( cityOfNiigata.hashCode() )

 そうして、これらのオブジェクトがデフォルトでもつ関数 hashCode() を呼び出してハッシュコードを得てコンソールに println() 関数で出力しています。
 実行結果はどちらも 804461605 のようになります。このことからオブジェクト 新潟市cityOfNiigata はどちらも同じものであると判断できます。

println( 新潟市.equals( cityOfNiigata ) )

 コード例では同じであるかの判断を equals() メソッドで行っています。 equals() メソッドは オブジェクト1.equals( オブジェクト2 ) のように記述して、2つのオブジェクト1とオブジェクト2が同じであるかの比較を行います。
 コード例では、オブジェクト 新潟市cityOfNiigataequals() メソッドで等しいかどうかを比較して、実行結果が true と表示されています。

println(
    都市( "静岡市", 699255 ).equals( 都市( "浜松市", 807130 ) )
)

 オブジェクトどおしの比較はインスタンス化しなくても行えます。コード例のようにデータクラスのコンストラクタ引数を与えた 都市( "静岡市", 699255 ) に直接 equals() メソッドを記述して、メソッドの引数に比較相手のデータクラスのコンストラクタ引数を与えた 都市( "浜松市", 807130 ) を直接指定することもできます。

copy() 関数

 データクラスのオブジェクトをコピーするには copy() 関数を使います。

copy() 関数の書式

 copy() 関数は以下のような書式で利用します。

copy()関数の書式(全プロパティをそのままコピーする場合)
val コピ先オブジェクト: タクラス名 = コピ元オブジェクト.copy()
または
val コピ先オブジェクト = コピ元オブジェクト.copy()
copy()関数の書式(プロパティを変更してコピーする場合)
val コピ先オブジェクト: タクラス名 = コピ元オブジェクト.copy( 変更するプロパティ名 = , ・・・)
または
val コピ先オブジェクト = コピ元オブジェクト.copy( 変更するプロパティ名 = , ・・・)

 コピー先のオブジェクトは不変変数 val キーワード、または可変変数 var キーワードで宣言します。このとき、データクラス名の型名の指定はコトリンの型推論により明らかですので、省略することができます。
 コピーするときに一部のプロパティの値を変更することができます。この場合には、 copy() 関数の引数に

変更したいプロパティ名 = 変更する値

のように指定します。変更したいプロパティ値が複数ある場合にはカンマ , で区切って記述します。

copy() 関数のコード例

 copy() 関数のコード例と実行結果を以下に示します。

copy()関数のコード例
data class ロボット( val 型番: String, val 名称: String, val ジョン: Float )

fun main( args: Array<String> ) {
  val 若菜 = ロボット( "TA9700", "葵若菜", 1.0F )

  val 旧型若菜: ロボット = 若菜.copy()
  println(
    "◇旧型若菜:" + 旧型若菜.型番 + ", " + 旧型若菜.名称 + ", " + 旧型若菜.ジョン )

  val 新型若菜 = 旧型若菜.copy( ジョン = 2.0F )
  var ( 型番, 名称, バジョン ) = 新型若菜
  println( "◇新型若菜:$型番, $名称, $バージョン" )
}

ideone :https://ideone.com/fork/0Eir9L
【paiza.io】ロボット.PNG

実行結果
◇旧型若菜:TA9700, 葵若菜, 1.0
◇新型若菜:TA9700, 葵若菜, 2.0

コード例の解説

 上記の copy() 関数のコード例について以下で説明します。

データクラスの定義

 型番名称バージョン をプライマリコンストラクタの引数に持つデータクラス ロボット を定義しています。ここで、これらの引数は不変変数キーワード val で宣言されています。データクラスのデフォルト関数を使うためには、引数を不変変数キーワード val または可変変数キーワード var を用いて宣言する必要がありますので注意が必要です。

main() 関数内の記述

データクラスのインスタンス化

val 若菜 = ロボット( "TA9700", "葵若菜", 1.0F )

 まず、データクラス ロボット にプライマリコンストラクタの引数を与えてインスタンス化し、オブジェクト 若菜 に代入しています。

copy() 関数の単純利用

val 旧型若菜: ロボット = 若菜.copy()

 オブジェクト 若菜copy() 関数を利用して、オブジェクト 若菜 をコピーしています。コピーしたオブジェクトを ロボット データクラス型のオブジェクト 旧型若菜 に代入しています。このコピーによって

( "TA9700", "葵若菜", 1.0F ) が ( "TA9700", "葵若菜", **2.0F** ) に変更

されて、オブジェクト 旧型若菜 に代入されます。

copy() 関数でプロパティの一部を変更

val 新型若菜 = 旧型若菜.copy( バージョン = 2.0F )

 オブジェクト 旧型若菜copy() 関数を利用して、オブジェクト 旧型若菜 をコピーしています。 コピーするさいに、データクラス ロボット のプロパティ バージョン を変更して 2.0F (浮動小数点の2.0)を設定しています。

多重宣言

 以下のコードで変数の多重宣言を行っています。多重宣言とは複数の変数をひとまとめに宣言することです。

var ( 型番, 名称, バージョン ) = 新型若菜

 これは、データクラス ロボット のオブジェクト 新型若菜 のプロパティを分解して各変数に代入しています。
 余談ですが、この ( 型番, 名称, バージョン ) のように変数や値のかたまりをコトリンと同種のJVM言語 Scala では タプル (Turple)と呼んでいます。旧バージョンのコトリンにはこの タプル がありましたが、最近のバージョンでは廃止されています。
 コード例では、オブジェクトを代入していますが、以下のように直接データクラスのインスタンス化宣言を代入することもできます。

var ( 型番, 名称, バージョン ) = ロボット( "Paruru777", "遥香改", 7.0F )

println() 関数

 オブジェクト 旧型若菜 の各プロパティをコンソールに出力する際に、以下のようにオブジェクト 旧型若菜 とプロパティ名をドット . でつないで println() 関数にわたしています。

println(
    "◇旧型若菜:" + 旧型若菜.型番 + ", " + 旧型若菜.名称 + ", " + 旧型若菜.バージョン )

 しかし、新型若菜では、上記の 多重宣言 のようにタプル形式で各プロパティ変数をまとめて設定しています。そうそて各プロパティ変数を 変数補間記号 $ を変数の前に記述して、文字列内に補間しています。

println( "◇新型若菜:$型番, $名称, $バージョン" )