1日目(第2章)の記事はこちら → EffectiveJava読書会1日目
今回は 第3章「すべてのオブジェクトに共通のメソッド」 についてやります。
項目8 equalsをオーバーライドする時は一般契約に従う
equalsをオーバーライドすべきとき、すべきでないとき
こんなクラスでequalsをオーバーライドするのは嫌だ(というか、必要ない)
- クラスのインスタンスが本質的に一意
- 論理的に同じかどうかを調べる必要がない
- スーパークラスでオーバーライドされているequalsがこのクラスに対しても適切に動く
- クラスがprivateでequalsが絶対に呼ばれない(→この場合はAssertionErrorなどをthrowすると良い)
じゃあどんなときにオーバーライドするの?
→ 論理的等価性の判定が必要で、スーパークラスにそれを満たすequalsが実装されていない場合
equalsを実装する時に満たさなければならない「一般的契約」
Objectの仕様によると、equalsメソッドは以下の5点を厳守しなければならない。
(x,y,zはnullではないとする)
-
反射的
x.equals(x)
が必ずtrue -
対称的
y.equals(x)
がtrueの時のみ、x.equals(y)
は必ずtrue -
推移的
x.equals(y)
とy.equals(z)
がtrueのとき、x.equals(z)
は必ずtrue -
整合的
equalsの判定で使う情報に変更がなければ、x.equals(y)
は常にtrueを返すか、常にfalseを返すかのどちらか -
非null性
x.equals(null)
は必ずfalse
自分とは異なるクラスからequalsが呼ばれることも頻繁にあるし、数多くのクラスがこの「契約」のもとに実装されている。これらを守らないと挙動がおかしくなったり、クラッシュしたりすることがある。
##質の高いequalsメソッドを実装するために
-
引数が自分自身のオブジェクトへの参照であるかを検査するのに==演算子を使う
-
引数が正しい型かを検査するのにinstanceof演算子を使う
-
引数を正しい型にキャストする
-
クラスの各「有意味な」フィールドに対して、引数のオブジェクトと自分自身のフィールドが一致するか調べる
ⅰ 基本データ型のフィールド:==演算子を利用
ⅱ **Float/Double:**Float.compare/Double.compareを利用
ⅲ 配列: 各要素ごとに判定 (ずべて比較するならArrays.equalsが使える)
ⅳ 参照の比較では正当なnullを比較することもあるが、その際NullPointerExceptionに気をつける -
equalsメソッドを実装したとき、equalsが「対照的・推移的・整合的」であるかを再度確認する
項目9 equalsをオーバーライドする時は、常にhashCodeをオーバーライドする
##なぜhashCodeをオーバーライドする必要が?
equalsをオーバーライドしているときにhashCodeをオーバーライドしないと、Object.hashCodeの「一般的契約」を破ることになり、ハッシュに基づくコレクション(HashMap、HashSet、HashTableなど)が正常に動作しなくなる
##hashCodeが満たすべき「契約」
- equalsの比較で使う情報が変更されない場合、ひとつのオブジェクトのhashCodeが何度呼ばれても(同一アプリケーションの一回の実行において)同じ整数値を返す
- 2つのオブジェクトがequalsで等しい場合、2つのオブジェクトのhashCodeは同じ整数値を返す
- 2つのオブジェクトがequalsで等しくない場合に別のhashCodeを返す必要はないが、同じhashCodeを返すような場合が多い場合、ハッシュテーブルのパフォーマンスの低下が起きる可能性がある
equalsを書き換えたときに「契約違反」となりやすいのは
「2. 2つのオブジェクトがequalsで等しい場合、2つのオブジェクトのhashCodeは同じ整数値を返す」
つまりequalsが等しくてもhashCode上では別オブジェクトと認識されてしまう
じゃあhashCodeをどう実装する?
適切なハッシュ関数を定義するのは難しい
最悪の「正当なハッシュ関数」
@Override public int hashCode() { return 42; } // 生命・宇宙・すべての答え!!!
すべてのインスタンスで同じ値を返すため、確かに上の3つの契約は満たすが、ハッシュテーブルが威力を発揮しなくなる(リンクリストと同じに……)
よくあるハッシュ関数の実装
1.result = (奇数の素数a)
で初期化
2. equalsの比較で利用しているフィールド 「のみ」 各フィールドごとに以下を計算
2-1. フィールドの情報を使ってintな(ハッシュ値c)
を計算
2-2. result = (奇数の素数b) * result + (ハッシュ値c)
各データ型ごとのハッシュ値の計算方法
boolean: (field ? 0 : 1)
byte, char, short, int: (int)field
long: (int)(field ^ (field >>> 32))
float: Float.floatToIntBits(field)
double: Double.doubleToLongBits(field)
→得られたlongを上記の方法でハッシュ計算
オブジェクト参照:フィールドのequalの再帰呼び出しでequalsが実装されている場合は再帰的にhashCodeを呼び出す
配列:各要素ごとにハッシュ値を求める
項目10 toStringを常にオーバーライドする
有用なtoStringとは?
toStringはデバッガの出力や、printlnにオブジェクトが渡されたときなどに呼び出される
その時に、その オブジェクトについての有用な情報(中身)がわかりやすく表示されるとうれしい
デフォルトのObjectのtoStringで表示される情報はこんな感じ
(クラス名)@(hashCodeの値)
これじゃあ内容がぜんぜんわからない……
だから、toStringは常にオーバーライドしてオブジェクトの中身が適切に表示されるようにする
toStringでの表示形式の意図をしっかりドキュメントに書くべし!
一度toStringの戻り値の表示形式を決めてしまうと、それ以降その形式を変更は実質不可能
項目11 cloneを注意してオーバーライドする
cloneの満たすべき「契約」
java.lang.Objectの仕様をまとめると...
・ cloneは オブジェクトのコピーを生成して返す(=新たなインスタンスを生成する)
・ オブジェクト内部のデータ構造もコピーされる必要があるかもしれない
・ コンストラクタは呼び出されない
大まかなには x.clone() != x
とか x.clone().getClass() == x.getlass()
とか x.clone().equals(x)
などがtrueになるということ(ただし、必須事項ではない)
正しく機能するcloneを実装するときの方針
基本方針(すべてのフィールドが基本データ型or不変オブジェクトのとき)
・スーパークラスのcloneメソッド(super.clone())を呼び出して、自分のクラスにキャストして返す
可変オブジェクトを参照しているフィールドを持つとき
・クラス中で参照しているフィールドのcloneメソッドも再帰的に呼び出す
ハッシュテーブルのclone
・ハッシュテーブルを構成するバケット配列の各バケット(リンクリスト)をdeepCopyする
その他の注意点
・ スレッドセーフなクラスにcloneを実装するときはcloneも適切に同期させなければならない
そこまでしてcloneを実装する必要があるの?
・Cloneableを実装しているクラスを拡張する場合は必須
・コピーコンストラクタ・コピーファクトリーを使うほうが懸命な場合が多い
・オブジェクトコピーをする必要がないケースもある(不変なオブジェクトのコピー機能は基本的に不要)
項目12 Comparableの実装を検討する
compareToをちゃんと実装すると、各インスタンスの自然な順序を定義できる
(→ソート済みのデータ構造(TreeSet, TreeMap)や検索・ソートができる(Collections, Arrays)などが正常に動作するようになる)
compareToの「一般契約」
-
すべてのx,yについて、
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
(y.compareTo(x)が例外を投げるときだけx.compareTo(y)が例外を投げる) -
推移的である x.compareTo(y) > 0, y.compareTo(z) > 0 ならば x.compareto(z) > 0
-
すべてのzについて、
x.compareTo(y)==0
のとき
sgn(x.compareTo(z)) == sgn(y.compareTo(z))
-
(x.compareTo(y) == 0) == (x.equals(y))
が強く推奨されるが厳密には必須ではない
(従っていない場合はちゃんとそれを明記する)
compareToの比較とequalsの比較
comparetoは「順序比較」、equalsは「同値比較」
compareToで比較する2つのインスタンスの型が違う場合はClassCastExceptionをスローする
(equalsでは異なるクラス同士で比較することもある)
compareToを実装するときの基本方針
基本フィールドの順序比較
関係演算子「>」か「<」で比較する
Float・Doubleの比較
Float.compareやDouble.compareを使って比較する
複数のフィールドを持つクラスの比較
各フィールドの重要性を考えて、一番重要なフィールドから比較して順序を決める (同じなら次に重要なフィールドで比較)