Java
Kotlin
EffectiveJava

Effective Java を Kotlin で読む(2):第3章 すべてのオブジェクトに共通のメソッド

effectivekotlin.png

章目次

Effective Java を Kotlin で読む(1):第2章 オブジェクトの生成と消滅
Effective Java を Kotlin で読む(2):第3章 すべてのオブジェクトに共通のメソッド 👈この記事
Effective Java を Kotlin で読む(3):第4章 クラスとインタフェース
Effective Java を Kotlin で読む(4):第5章 ジェネリックス
Effective Java を Kotlin で読む(5):第6章 enum とアノテーション
Effective Java を Kotlin で読む(6):第7章 メソッド
Effective Java を Kotlin で読む(7):第8章 プログラミング一般
Effective Java を Kotlin で読む(8):第9章 例外
Effective Java を Kotlin で読む(9):第10章 並行性
Effective Java を Kotlin で読む(10):第11章 シリアライズ

第3章 すべてのオブジェクトに共通のメソッド

項目8 equals をオーバーライドする時は一般契約に従う

概要

equals を間違った方法でオーバーライドすると悲惨な結果となる。問題を避ける最も簡単な方法はオーバーライドしないことである。

クラスが論理的等価性を持っている場合のみ equals をオーバーライドすべきであり、その場合は以下の一般契約を厳守する。

  • 反射性(reflexivity)
  • 対称性(symmetry)
  • 推移性(transitivity)
  • 整合性(consistency)
  • 非 null 性(non-nullity)

詳細は Object.equals の javadoc を参照

Kotlin で読む

書籍にも書かれているが equals が必要なのは、大体は値クラスの場合である。そして Kotlin では値クラスを言語でサポートしている。

Kotlin の Data Class では、プライマリコンストラクタを元に以下のメンバを自動的に推論する。

  • equals() / hashCode() のペア
  • "User(name=John, age=42)" 形式の toString()
  • 宣言した順番でプロパティに対応する componentN() 関数
  • copy() 関数
data class User(val name: String, val age: Int)

// こう使う
println(User("John", 42) == User("John", 42))

よって equals を自分で記述する機会はそう無いはず。ただし、メンバに配列がある場合は要注意。

data class Book(val title: String, val authors: Array<String>)

// false が出力される
println(Book("book", arrayOf("Alice","Bob")) == Book("book", arrayOf("Alice","Bob")))

Try Kotlin で確認

これは java の配列が Object.equals() をオーバーライドしておらず、参照を比較してしまう為である。この場合は自分で equals を実装することが必要となる。

結論として、値クラスにおいては Array の代わりに List を使うのが賢明に思う。

ちなみにこの問題は Kotlin の開発陣も認識しているが、Java で解決されない限りしょうがないとの事。

We’d love to fix the inconsistency with collections, but the only sane way of fixing it seems to be fixing it in Java first, which is beyond anybody’s power, AFAIK :)
https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/

項目9 equals をオーバーライドする時は、常に hashCode をオーバーライドする

概要

Object.hashCode の一般契約を守るために equals をオーバーライドしているすべてのクラスで、hashCode をオーバーライドしなければならない。これを破ると例えば以下のような問題が発生する。

val map: HashMap<Piyo, String> = hashMapOf(Piyo(1) to "one")
println(map[Piyo(1)]) // null になる

hashCode の規約は以下の通り。

  • 値の変更のない同じオブジェクトの hashCode は、同じ整数を返す
  • equals で等しいオブジェクトの hashCode は、同じ整数を返す
  • equals で等しくないオブジェクトの hashCode は、異なる整数を返す必要はない

詳細は Object.hashCode の javadoc を参照

Kotlin で読む

equals() 同様、Data Class であれば自動的に生成してくれるので、基本的にはこれで十分。

また、配列を含んでいても hashCode() は構造的に評価してくれるので equals() で異なっても同じ hashCode になる。

val b1 = Book("book", arrayOf("Alice","Bob"))
val b2 = Book("book", arrayOf("Alice","Bob"))
println(b1 == b2) // false
println(b1.hashCode() == b2.hashCode()) // true

Try Kotlin で確認

ちなみにプライマリコンストラクタにないメンバは計算に含まれないので注意。(equals も同様)

項目10 toString を常にオーバーライドする

概要

toString が返す文字列は、「人間が読める簡潔で有益な情報であるべき」である。
Object.toString 参照
java.lang.Object の toString の実装は、 "PhoneNumber@163b91" のようにユーザが見たい内容では無いため、より良い toString 実装を提供することが必要。

Kotlin で読む

Data class は "User(name=John, age=42)" 形式の toString() を自動的に生成してくれる。

自分で実装するとしても String Template で Java よりは楽に書ける。

class User(val name: String, val age: Int) {
    override fun toString() = "name: $name, age: $age"
}

StringBuilder の拡張関数である buildString も便利。

class User2(val name: String, val age: Int) {
    override fun toString() = buildString {
        append("name: $name, ")
        append("age: $age")
    }
}

Try Kotlin で確認

項目11 clone を注意してオーバーライドする

概要

複製可能なクラスである事を表す為 Cloneable インタフェースを継承した上で clone メソッドを実装する必要がある。(ただし Cloneable 自体は空のインタフェースであり、これを継承すると clone メソッドの挙動が変わるという、特殊であまり良くない実装となっている)

オブジェクトのコピーは複雑なテーマであり、書籍ではかなりの文章量で Cloneable を実装する際の注意点が書かれている。また一般的には、代替手段としてコピーコンストラクタコピーファクトリーを提供し、 Cloneable は実装しない事が賢明であるとしている。

Kotlin で読む

Kotlin では、 Data class においてコピーファクトリーとして copy() が自動的に生成される。やはり通常はデータクラスの利用が賢明。

val tom = User("Tom", 20)
val bob = tom.copy(name = "Bob")

println(bob) // User(name=Bob, age=20)

Try Kotlin で確認

どうしても Cloneable を提供したい場合は、kotlin.Cloneableを継承し、 clone メソッドを実装する。

class Example : Cloneable {
    override fun clone(): Any { ... }
}

Do not forget about Effective Java, 3rd Edition, Item 13: Override clone judiciously. Calling Java code from Kotlin#clone()

ただし kotlin-docs 内でも釘を刺されているように、正確に理解した上で実装する事。

項目12 Comparable の実装を検討する

概要

値クラスが自然な順序(natural ordering)を持っているならば、Comparable の実装を検討すると良い。

その際は、compareTo の一般契約に従うこと。(equals の一般契約と矛盾しないよう注意)

Kotlin で読む

kotlin では kotlin.Comparable を実装する事になる。

class Date(val year: Int, val month: Int, val day: Int) : Comparable<Date> {
    override fun compareTo(other: Date): Int {
        if (this.year > other.year) return 1
        if (this.year < other.year) return -1
        if (this.month > other.month) return 1
        if (this.month < other.month) return -1
        if (this.day > other.day) return 1
        if (this.day < other.day) return -1
        return 0
    }
}

compareTo を実装するのは Java と同じだが、拡張関数がいくつか定義されており便利。
特に拡張関数 rangeTo により以下のように Range への変換が可能になる。

val r1 = Date(2018, 1, 1).rangeTo(Date(2018, 12, 31))
// Operator overloading により以下も可
val r2 = Date(2018, 1, 1)..Date(2018, 12, 31)

// このように使える
println(Date(2018, 7, 7) in r2)

ちなみに comapreTo も演算子オーバーロードされているので、以下のように使える。

println(Date(2018, 8, 30) > Date(2018, 9, 1))
println(Date(2018, 8, 30) < Date(2018, 9, 1))

Try Kotlin で確認

おわり

参考資料等