章目次
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")))
これは 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
ちなみにプライマリコンストラクタにないメンバは計算に含まれないので注意。(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")
}
}
項目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)
どうしても 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))
おわり
- 前: Effective Java を Kotlin で読む(1):第2章 オブジェクトの生成と消滅
- 次: Effective Java を Kotlin で読む(3):第4章 クラスとインタフェース
参考資料等
- Kotlin Reference
- Kotlin Language Documentation
- Effective Java 第2版
- Kotlin イン・アクション
- [(Qiita)JavaプログラマがKotlinでつまづきがちなところ] (https://qiita.com/koher/items/d9411a00986f14683a3f)
- [(Blog)data classで配列を使う時はequalsをオーバーライドしよう [Kotlin]] (http://scache.hatenablog.com/entry/2018/03/21/004926)
- (Blog)Feedback Request: Limitations on Data Classes
- [(Qiita)Kotlin data classのhashCodeの実装] (https://qiita.com/u_nation/items/888f6083b6fd1d5cf72b)