はじめに
下記のような継承関係にあるクラスのequals()メソッドを普通に実装しただけでは、equals()が満たすべき性質が満たせない(補足1)ということが知られている。*1参考
同参考文献にはその回避策も記載されている(補足2)が、別の文献*2参考からの解決策を記載する。
結論
下記のような2引数のequals()静的メソッドを作成し、それを通してObject.equals()を呼び出す。重要なのは両クラスのequals()を呼び出す部分。これはこのクラス構成以外でも使用可能。全文
ちなみに標準APIに Objects.equals(Object, Object) があるが、左辺のequals()しか呼び出していない。
ObjectUtil.java
final class ObjectUtil {
private ObjectUtil() { throw new AssertionError("Not intended to be instanciated"); }
public static boolean equals(final Object lhs, final Object rhs) {
// 左辺・右辺の参照先が同じなら同一
if (lhs == rhs) return true;
// 左辺・右辺の参照先が共にnullなら同一
if (lhs == null && rhs == null) return true;
// 左辺・右辺の参照先のどちらかがnullなら不同
if (lhs == null || rhs == null) return false;
// 左辺・右辺の参照先が同一クラスならそのクラスのequals()を返却する
if (lhs.getClass() == rhs.getClass()) return lhs.equals(rhs);
// 左辺・右辺の参照先が別のクラスなら両クラスのequals()のandを返却する
return lhs.equals(rhs) && rhs.equals(lhs);
}
}
補足1
// 1. 対称性が満たせない
// -> 右辺・左辺を入れ替えると結果が異なる
// 上図のクラス構成を用いた例
Point1D p1 = new Point1D(0);
Point2D p2 = new Point2D(0, 0);
assert(p1.equals(p2) == true); // x値しか比較しないためtrue
assert(p2.equals(p1) == false); // Point2Dのinstanceであることを判定するためfalse
// 2. 推移性が満たせない
// -> p1 == p2 && p2 == p3 の場合でも、p1 == p3 とならない
// 上図のクラス構成を用いた例
// 前提: Point2D.equals() 仕様に下記を追加しておく
// 引数が、Point1Dまたはそのサブクラス、かつx値が同じ場合でも、true
Point2D p1 = new Point2D(0, 0);
Point1D p2 = new Point1D(0);
Point2D p3 = new Point2D(0, 1);
assert(p1.equals(p2) == true); // 新仕様のためtrue
assert(p2.equals(p3) == true); // x値の比較しないためtrue
assert(p1.equals(p3) == false); // x,y値を比較するためfalse
// 3. リスコフの置換原則が満たせない
// -> サブクラスでも同様の動きが想定されるが、そうならない
// 上図のクラス構成を用いた例
// 前提1: Point1D.equals() 仕様を下記のように変更する
// 引数が、Point1D、かつx値が同じ場合のみ、true
// 前提2: CountingPoint1D.equals() 仕様を下記のように変更する
// 引数が、CountingPoint1D、かつx値が同じ場合のみ、true
Point1D p1 = new Point(0);
CountingPoint1D cp1 = new CountingPoint(0);
CountingPoint1D cp2 = new CountingPoint(0);
List<CountingPoint1D> list = Arrays.asList(cp1);
assert(list.contains(cp2) == true); // cp1 == cp2 のためtrue
assert(list.contains(p1) == false); // 前提1のためfalse
補足2
// 継承ではなくコンポジションを用いる
// 例
class Point2D {
private final Point1D p1;
private final int y;
...
@Override
public boolean equals(Object o) {
if (!(o instanceof Point2D)) return false;
Point2D rhs = (Point2D)o;
return p1.equals(rhs.p1) && y.equals(rhs.y);
}
}
*1
ジョシュア・ブロック(平成26年)「Effective Java 第2版」丸善. 項目8
*2
アンドレイ・アレキサンドレスク(2013)「プログラミング言語D」翔泳社. 6.8.3