LoginSignup
20
20

More than 5 years have passed since last update.

EffectiveJava読書会2日目

Posted at

1日目(第2章)の記事はこちら → EffectiveJava読書会1日目

今回は 第3章「すべてのオブジェクトに共通のメソッド」 についてやります。

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

equalsをオーバーライドすべきとき、すべきでないとき

こんなクラスでequalsをオーバーライドするのは嫌だ(というか、必要ない)

  1. クラスのインスタンスが本質的に一意
  2. 論理的に同じかどうかを調べる必要がない
  3. スーパークラスでオーバーライドされているequalsがこのクラスに対しても適切に動く
  4. クラスがprivateでequalsが絶対に呼ばれない(→この場合はAssertionErrorなどをthrowすると良い)

じゃあどんなときにオーバーライドするの?
→ 論理的等価性の判定が必要で、スーパークラスにそれを満たすequalsが実装されていない場合

equalsを実装する時に満たさなければならない「一般的契約」

Objectの仕様によると、equalsメソッドは以下の5点を厳守しなければならない。

(x,y,zはnullではないとする)
1. 反射的
 x.equals(x)が必ずtrue
2. 対称的
 y.equals(x)がtrueの時のみ、x.equals(y)は必ずtrue
3. 推移的
 x.equals(y)y.equals(z)がtrueのとき、x.equals(z)は必ずtrue
4. 整合的
 equalsの判定で使う情報に変更がなければ、x.equals(y)は常にtrueを返すか、常にfalseを返すかのどちらか
5. 非null性
 x.equals(null)は必ずfalse

自分とは異なるクラスからequalsが呼ばれることも頻繁にあるし、数多くのクラスがこの「契約」のもとに実装されている。これらを守らないと挙動がおかしくなったり、クラッシュしたりすることがある。

質の高いequalsメソッドを実装するために

  1. 引数が自分自身のオブジェクトへの参照であるかを検査するのに==演算子を使う
  2. 引数が正しい型かを検査するのにinstanceof演算子を使う
  3. 引数を正しい型にキャストする
  4. クラスの各「有意味な」フィールドに対して、引数のオブジェクトと自分自身のフィールドが一致するか調べる
     ⅰ 基本データ型のフィールド:==演算子を利用
     ⅱ Float/Double:Float.compare/Double.compareを利用
     ⅲ 配列: 各要素ごとに判定 (ずべて比較するならArrays.equalsが使える)
     ⅳ 参照の比較では正当なnullを比較することもあるが、その際NullPointerExceptionに気をつける

  5. equalsメソッドを実装したとき、equalsが「対照的・推移的・整合的」であるかを再度確認する

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

なぜhashCodeをオーバーライドする必要が?

equalsをオーバーライドしているときにhashCodeをオーバーライドしないと、Object.hashCodeの「一般的契約」を破ることになり、ハッシュに基づくコレクション(HashMap、HashSet、HashTableなど)が正常に動作しなくなる

hashCodeが満たすべき「契約」

  1. equalsの比較で使う情報が変更されない場合、ひとつのオブジェクトのhashCodeが何度呼ばれても(同一アプリケーションの一回の実行において)同じ整数値を返す
  2. 2つのオブジェクトがequalsで等しい場合、2つのオブジェクトのhashCodeは同じ整数値を返す
  3. 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の「一般契約」

  1. すべてのx,yについて、sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
    (y.compareTo(x)が例外を投げるときだけx.compareTo(y)が例外を投げる)

  2. 推移的である x.compareTo(y) > 0, y.compareTo(z) > 0 ならば x.compareto(z) > 0

  3. すべてのzについて、x.compareTo(y)==0のとき
    sgn(x.compareTo(z)) == sgn(y.compareTo(z))

  4. (x.compareTo(y) == 0) == (x.equals(y))が強く推奨されるが厳密には必須ではない
    (従っていない場合はちゃんとそれを明記する)

compareToの比較とequalsの比較

comparetoは「順序比較」、equalsは「同値比較」

compareToで比較する2つのインスタンスの型が違う場合はClassCastExceptionをスローする
(equalsでは異なるクラス同士で比較することもある)

compareToを実装するときの基本方針

基本フィールドの順序比較
関係演算子「>」か「<」で比較する
Float・Doubleの比較
Float.compareやDouble.compareを使って比較する
複数のフィールドを持つクラスの比較
各フィールドの重要性を考えて、一番重要なフィールドから比較して順序を決める (同じなら次に重要なフィールドで比較)

20
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20