Effective Javaを勉強します【第3章】
Objectクラスのequals、hashCode、toString、clone、finalizeはすべて オーバーライドされるように設計されているため、明示的な一般契約がある。
これに従わないと、この契約に依存しているHashMapやHashSetなど他のクラスが 適切に機能しない恐れがあるため、いつ、どのようにオーバーライドすべきか学びましょうと いう話。 (ComparableはObjectではないが同様の特徴をもつため、本章で扱う)
項目8. equalsをオーバーライドする時は一般契約に従う
equals()
をオーバーライドすべきでない場合
以下の条件のいずれかに当てはまる場合は、equals()
をオーバーライドしない方が適切。
- クラスの個々のインスタンスは本質的に一意である
- Threadなどのように、値よりは能動的な実態を表しているクラスの場合
- Objectクラスの提供する
equals()
の振る舞いが正しい- Objectの提供する
equals()
の実装は以下になってる。参照が同じである=等しいと判断される。
- Objectの提供する
public boolean equals(Object obj) { return (this == obj); }
「論理的等価性」検査をクラスが提供するかどうかに関心がない
- 例えばRandomインスタンスがこれから同じ乱数列を生成するか検査をする必要はない
- この場合もObjectの提供する
equals()
の振る舞いが正しい
スーパークラスがすでにequalsをオーバーライドしており、スーパークラスの振る舞いがこのクラスに対して適切である
- 例えばSetならAbstractSet、ListならAbstractList、MapならAbstractMap
クラスがprivateあるいはパッケージプライベートであり、そのequalsメソッドが決して呼び出されないことが確かである
- 万が一呼び出された場合に備えて、以下のように例外をスローするようオーバーライドする
@Override public boolean equals (Object obj) { throw new AssertionError(); }
equals()
をオーバーライドすべき場合
クラスが論理的等価性という概念を持っていて、スーパークラスがequals()
をオーバーライドしていないとき。
- 論理的等価性=論理的に同値であるのかを知りたい
- オブジェクトの同一性(参照先が同じ)を確かめたいのではなく、持つ値が正しいのかを知りたい
equalsをオーバーライドする時は、以下の一般契約を厳守しなければならない
反射性
nullでない任意の参照値xに対して、x.equals(x)
はtrueを返さなければならない。
- オブジェクトがそれ自身と等しくなければならないということ
- これを意図的に破ることは難しい
対称性
nullでない任意の参照値xとyに対して、y.equals(x)
がtrueを返す場合のみ、x.equals(y)
はtrueを返さなければならない。
以下の例のように簡単に破ることが可能なので注意。
public class CaseInsensitiveString { private final String s; public CaseInsensitiveString(String s) { if (s == null) { throw new NullPointerException(); } this.s = s; } // 間違った例 @Override public boolean equals(Object o) { if (o instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) o).s); if (o instanceof String) // 引数がStringの場合、大文字小文字を無視して比較しているが // Stringクラスでは無視しないため対称性が成り立たない return s.equalsIgnoreCase((String) o); return false; } // 正しい例 @Override public boolean equals(Object o) { // クラスが異なる場合はfalseを返すのが正解(Stringは比較しない) return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); } }
推移性
nullでない任意の参照値x、y、zに対して、x.equals(y)
とy.equals(z)
がtrueを返すなら、x.equals(z)
はtrueを返さなければならない。
- インスタンス化可能なあるクラスを拡張して、equals契約を守ったまま値要素を追加したサブクラスを実装する方法はないので注意
- あるクラスに値要素を追加したい時は、継承ではなくコンポジションを用いるべきである(項目16)
例) 2次元座標を保持したPointクラスと、Pointクラスを継承して色情報を付加したColorPointというサブクラスを考える。
// スーパークラス public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (!(o instanceof Point)) { return false; } Point p = (Point) o; return p.x == this.x && p.y == this.y; } } // サブクラス public class ColorPoint extends Point { private Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } }
このときColorPointクラスでequalsをオーバーライドしなければ、ColorPointのequals比較では色情報が無視されてしまうため、 色情報も一致したときにtrueを返すよう以下のように実装することを考える。
// ColorPointの誤ったequals()の例1 // 対称性が守られていない @Override public boolean equals(Object o) { if (!(o instanceof ColorPoint)) { return false; } return super.equals(o) && ((ColorPoint) o).color == color; }
これは以下のように比較を行った場合、対称性が守られないため不適切。
Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, Color.RED); p.equals(cp); // => true(色情報が無視される) cp.equals(p); // => false
cp.equals(p)
がtrueを返すよう、以下のようにColorPointクラスとPointクラスの比較の際は色情報を無視するよう実装すると、推移性が守られない。
// ColorPointの誤ったequals()の例1 // 推移性が守られていない @Override public boolean equals(Object o) { if (!(o instanceof Point)) { return false; } // oがPointクラスの場合は、Pointクラスのequals()で色情報を無視した比較を行う if (!(o instanceof ColorPoint)) { return o.equals(this); } return super.equals(o) && ((ColorPoint) o).color == color; }
これは以下のように比較を行った場合、推移性が守られないため不適切。
ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE); p1.equals(p2); // => true p2.equals(p3); // => true p1.equals(p3); // => false
以下のようにコンポジションを用いることで、equals契約を守ったままPointクラスに色情報を付加できる。
(ColorPointクラスはPointのサブクラスではないためcp.equals(p)
もp.equals(cp)
もfalseになる)
public class ColorPoint { private Point point; private Color color; public ColorPoint(int x, int y, Color color) { if (color == null) { throw new NullPointerException(); } point = new Point(x, y); this.color = color; } // ビューを返す public Point asPoint() { return point; } @Override public boolean equals(Object o) { if (!(o instanceof ColorPoint)) { return false; } ColorPoint cp = (ColorPoint) o; return cp.point.equals(point) && cp.color.equals(color); } }
ただし抽象クラスのサブクラスには、equals契約を守ったまま値要素の追加が可能。 (スーパークラスはインスタンス化されないため、サブクラスと比較されて推移性に違反することがないため)
整合性
nullでない任意の参照値xとyに対して、オブジェクトに対するequals比較に使用される情報が変更されなければ、
x.equals(y)
は常に同じ値を返し続けなければならない。
- 信頼できない資源に依存するequalsメソッドを書いてはならない
- 例)
java.net.URL
のequalsメソッドは、比較対象URLに関連づけられたホストのIPに依存している- ホストをIPに変換する時ネットワークへのアクセスが必要であり、常に同じ結果が保証されないため不適切
- メモリ中のオブジェクトのみに依存する処理を行うべき
- 例)
非null性
nullでない任意の参照値xに対して、x.equals(null)
はfalseを返さなければならない。
- nullが渡されたときにNullPointerExceptionがスローされるのは契約違反なので注意(falseを返すようにする)
高品質なequalsメソッドの書き方
- 引数が自分自身のオブェクトであるかどうかを検査する場合は == を利用する
- 引数が正しい型であるかを調べるためには instanceof を使う。
- 正しい型とは、普通はメソッドが定義されているクラスである。場合によっては、このクラスによって実装されているインターフェース。
- 引数を正しい型にキャストする
- そのインスタンスのフィールドと、与えられたオブジェクトのフィールドを検査する
- 基本データ型には
==
を、オブジェクトには再帰的にequals
を用いる - ただし、floatとdoubleには
Float.compare
とDouble.compare
を利用する。 - 配列のフィールドは
Array.equals
が利用できる。
- 基本データ型には
- 「対称性」、「推移性」、「整合性」の性質を満たしたかどうかのテストを書く
- 「反射性」と「非 null 性」は心配しなくても大抵の場合満たされる。
項目9. equalsをオーバーライドする時は、常にhashCodeをオーバライドする
equals
をオーバーライドしているすべてのクラスで、hashCode
をオーバーライドしなければならない。
- 上記を守っていないクラスがHashmap、HashSet、Hashtableを含む、すべてのハッシュに基づくコレクションで用いられると適切に機能しない
守らなければならない契約は以下。
equals
比較で使用されるオブジェクトの情報が変更されない限り、hashCode
メソッドは同じオブジェクトに対して常に同じ整数を返さなければならない- 2つのオブジェクトが
equals
比較により等しければ、2つのオブジェクトの個々に体するhashCode
メソッド呼び出しは、同じ整数を返さなければならない - 2つのオブジェクトが
equals
比較により等しくない場合、2つのオブジェクトの個々に体するhashCode
メソッド呼び出しが別の整数を返さなければならない、ということは要求されない。しかし別の整数を返す場合、ハッシュテーブルのパフォーマンスが改善される可能性がある。
hashCode
をオーバーライドするのを忘れた場合、上記2が破られる。
例) hashCode
をオーバーライドしていないPhoneNumberClass
public class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "areaCode"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "line number"); this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } private static void rangeCheck(int arg, int max, String name) { if (arg < 0 || max < arg) { throw new IllegalArgumentException(name + ": " + arg); } } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber phoneNumber = (PhoneNumber) o; return phoneNumber.lineNumber == this.lineNumber && phoneNumber.prefix == prefix && phoneNumber.areaCode == areaCode; } // hashCode がない! }
put
とget
で利用される2つのPhoneNumberクラスのハッシュコードが一致しないため、下記のようにHashMapが正しく動作しない。
Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); m.get(new PhoneNumber(707, 867, 5309)); // => null
以下の手順に従うことで、理想的なハッシュ関数を計算することが可能。
- 何らかのゼロでない定数、例えば 17 を
result
という int 変数に保存する。 - オブジェクト内の
equals
メソッドで考慮されるフィールドf
に対して以下のことを行う。- 各フィールドに対する intのハッシュコード
c
を計算する。- boolean ならば、
(f ? 0: 1)
を計算する。 - byte, char, short, int ならば
(int) f
を計算する。 - long ならば、
(int)(field ^ (f >>> 32))
を計算する。 - float ならば、
Float.floatToIntBits(f)
を計算する。 - double ならば
Double.doublToLongBits(f)
を計算し、結果の long を6と同様にハッシュする。 - オブジェクト参照で、
equals
内でフィールドをequals
の再帰的呼び出しによって比較しているならば、再帰的にhashCode
を呼び出す。フィールドがnull
なら0を返す。 - 配列ならば各要素を別々のフィールドとして取り扱い、各意味のある要素に対して再帰的にこれらの規則を適用してハッシュを計算する。要素全てに意味がある場合、
Arrays.hashCode
メソッドの利用が可能。
- boolean ならば、
- 上記で計算した
c
をresult = 31 * result + c;
の式に代入する。
- 各フィールドに対する intのハッシュコード
result
を返す。- 等しいインスタンスが等しいハッシュコードを持つかどうかを自問し、単体テストを書く。
もしクラスが不変でハッシュコードを計算するコストが高いならば、オブジェクト内にハッシュコードをキャッシュしておくことを検討すべきである。
もしそのクラスのオブジェクトのほとんどがハッシュキーとして利用される場合、以下のいずれかの対策をすべきである。
- インスタンス生成時にハッシュコードを計算する
- 最初に
hashCode
が呼び出された時に、遅延初期化をする(項目71)
項目10. toStringを常にオーバーライドする
toString
メソッドのデフォルト実装は、"クラス名@ハッシュコードの符号なし16進数表現"の文字列を返す。
→ 分かりづらいので、オーバーライドしてオブジェクトに含まれる有益な(ユーザーが興味があるであろう)情報を全て返すようにすべき。
toString
をオーバーライドする時に考慮すべき点として以下が挙げられる。
- ドキュメンテーション(javadoc)で戻り値の形式を明示するかどうか
- 明示することでユーザーに正確な表現形式を伝えることができる
- 一方で、明示した表現形式に依存した利用がされている可能性を考えると、その後の仕様変更がしづらくなるというデメリットもある
- 明示する、しないに関わらず、その意図は正確にjavadocに記載すべき
toString
の戻り値に含まれる全ての情報への、プログラミングによるアクセス手段を提供すべき
項目11. cloneを注意してオーバーライドする
clone
の一般契約は以下の通り。
- 「コピー」の正確な意味はオブジェクトのクラスに依存する
- 大まかな意図は以下が成り立つことだが、絶対的な要件ではない
- 任意のオブジェクト x に対して
x.clone() != x
が true であること - また
x.clone().getClass() == x.getClass()
も true であること x.clone().equals(x)
が true であること- そのクラスの新たなインスタンスを生成することに加え、内部データ構造のコピーも必要な場合がある
- コンストラクタは呼び出されない
- 任意のオブジェクト x に対して
clone
メソッドをオーバーライドするならば、super.clone
により得られたオブジェクトを返すべき
以下の例を考える。
public class Data1 implements Cloneable { ... @Override public Object clone() throws CloneNotSupportedException { return new Data1(x); } ... } public class Data2 extends Data1 { ... @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } ... }
上記の場合、Data2.clone
を呼び出すとsuper.clone
によりData1
のコンストラクタが返却される。
したがって「x.clone().getClass() == x.getClass()
も true であること」が成り立たない。
これはData1
クラスでも同様に、clone
でsuper.clone
を返すようにすることで解決する。
また上記でclone
メソッドのアクセス修飾子が public となっていることも大切である。
本書に書いてある通り、Cloneable
を実装しているクラスは、適切に機能している public のclone
メソッドを提供することが期待されている。
これらを踏まえて、java1.5からは下記のようにclone
メソッドを修正することができる。
public class Data1 implements Cloneable { ... @Override public Data1 clone() { try { return (Data1)super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(); } } ... } public class Data2 extends Data1 { ... @Override public Data2 clone() { try { return (Data2)super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(); } } ... }
上記の修正したclone
メソッドにおいて、注目すべき点が3点ある。
- 修正した
clone
の戻り値型がData1/Data2
になっている- java1.5から導入された共変戻り値型によるもの(オーバーライドしたメソッドの戻り値型は元のメソッドの戻り値型のサブタイプであってもよい)
super.clone
の戻り値は親クラスであるため、キャストを忘れずに行う(キャストはクライアントでなくライブラリーですべき処理)CloneNotSupportedException
のスロー宣言が削除されている- チェックされる例外をスローしない方が使い勝手が良い(publicの
clone
メソッドはスローしないべき) - 継承されることを想定したクラスで
clone
をオーバーライドする場合、clone
を protected と宣言し、CloneNotSupportedException
のスロー宣言をし、Cloneable
は実装しないようにする(Object.clone
をまねるべき)
- チェックされる例外をスローしない方が使い勝手が良い(publicの
フィールドに可変オブジェクトが含まれる場合は、再帰的にclone
を呼び出す
下記の例を考える。
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } @Override public Stack clone() { try { Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } ... }
上記のclone
メソッド内のsuper.clone
では、elementsフィールドの参照がコピーされるだけである。
(浅いコピー。元のオブジェクトとclone
オブジェクトで同一の配列が共有されてしまう。)
したがってelements.clone()
のように、再帰的にclone
を呼び出す必要がある。
しかしelementsフィールドがfinalの場合、新たな値を代入することができないため正しく機能しない。
この場合は、フィールドのfinal修飾子を取り除く必要がある。(clone
のアーキテクチャは可変オブジェクトを参照している final のフィールドと両立しない)
内部クラスを保持するなど、クラスの構造が複雑な場合、上記のように再帰的にclone
を呼び出すだけでは不十分な場合もある。
その時はsuper.clone
を呼び出し、返されたオブジェクトのすべてのフィールドを初期化した上で、オブジェクトの状態を再現するために
「深いコピー」を実現するための処理を呼び出すべき。
一般的に、クラスが基本データ型のフィールドか不変オブジェクトへの参照しか保持していない場合、上記のような「深いコピー」のための処理は不要。
clone
が本当に必要かよく検討する
これまで述べたようにclone
の実装は気をつけるべき点が多く複雑であり、本当に必要なケースは稀である。
(配列のコピー用途以外ではclone
を利用しないという人も多い)
以下のいずれかの手段で代替できないかよく検討する。
- コピーコンストラクタかコピーファクトリーを提供する
java public Yum(Yum yum); //コピーコンストラクタ public static Yum newInstance(Yum yum); //コピーファクトリー
- そもそもオブジェクトの複製機能を提供しない
項目12. Comparableの実装を検討する
Comparable
を実装することで、インスタンスが自然な順序を持っていることを明示する。
→アルファベット順、数値順、年代順など、明らかに自然な順序を持つ値クラスを書く場合、Comparable
を実装しcompareTo
メソッドを提供する。
compareTo
メソッドの一般契約は以下。これを守らないと、compareTo
に依存しているTreeSet
やTreeMap
、検索とソートアルゴリズムを含む
Collections
やArrays
が正しく機能しない。
(sgn(expression)
は、expression
が負の場合 -1、ゼロの場合 0、正の場合 1を返す関数)
- すべての x と y に関して
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
を保証しなければならないx.compareTo(y)
が例外をスローする( y のクラスは x と比較できない)場合のみ、y.compareTo(x)
は例外をスローしなければならない
(x.compareTo(y) > 0 && y.compareTo(z) > 0)
がtrueならばx.compareTo(z) > 0
もtrueでなければならない- すべての z に関して
x.compareTo(y) == 0
ならばsgn(x.compareTo(z)) == sgn(y.compareTo(z))
でなければならない (x.compareTo(y) == 0) == x.equals(y)
は強く推奨されるが必須ではない(破る場合は明示すべき)
上記制限はequals
制限と同じく、反射性、対称性、推移性に従わなければならないことを示している。
したがってequals
と同様に、インスタンス化可能なクラスを拡張してcompareTo
契約を守ったまま新たな値要素を追加する方法はなく、コンポジションが推奨される。
compareTo
は以下にしたがって実装すると良い。