data classで配列を使う時はequalsをオーバーライドしよう [Kotlin]

data classとは

data classは以下のようにクラスにdataをつけて宣言することで、
コンストラクタで宣言されたプロパティを使ったequals/hashCode/toString関数などを自動生成してくれます。

data class Book(
    val title: String,
    val page: Int
)

val book1 = Book("book", 100)
val book2 = Book("book", 100)
book1.toString()  // => Book(title=book, page=100)
book1 == book2    // => true

toString()で返って来る文字列にプロパティの値が入っていたり、
==(equalsが呼ばれる)を使った比較ができたりします。

data class内で配列を宣言した場合

↓のようにプロパティに配列型を宣言した場合にも、同様にequals()などの関数が自動で生成されます。

data class Book(
    val title: String,
    val author: Array<String>
)
    
val book1 = Book("book", arrayOf("Alice","Bob"))
val book2 = Book("book", arrayOf("Alice","Bob"))
book1.toString() // Book(title=book, author=[Alice, Bob])
book1 == book2   // false

しかし、先ほどとは違いbook1 == book2の結果がfalseになってしまいます。

なぜ配列を使うと比較がうまくできないのか?

配列を使ったdata classをJavaデコンパイルすると、
Array<String>String[]に、data classのequals内の配列の比較はarray1.equals(array2)と同等のコードになります。

Javaの配列型のequals()メソッドは、要素の比較は行わず配列型のインスタンスが同じかどうかを判定するため、配列内の値が等しいだけではtrueになりません。

対処法

equals()メソッドをオーバーライドしてArrays#equlas()を使うことでこの問題を解決することが可能です。

IntellijやAndroidStudioでKotlinPluginを入れている場合(最近のバージョンでは初めから入っています)は、Inspectionが表示され以下のようなequals()/hashCode()を自動生成することが可能になっています。 f:id:scache:20180321002505p:plain

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false
    other as Book
    if (title != other.title) return false
    if (!Arrays.equals(author, other.author)) return false
    return true
}
override fun hashCode(): Int {
    var result = title.hashCode()
    result = 31 * result + Arrays.hashCode(author)
    return result
}

最後に

data classで配列の比較も良い感じに生成してくれるとありがたいですが、影響がでかいためv2が来るまで変更されることはないと個人的には思います。