放課後プログラミング

調べたことや考えたことなどを忘れないために書きます。

equalsをオーバーライドするときに注意すること

『Effective Java 第2版』第8項目のまとめです
僕の理解のために実装を意味的に同じ範囲で書き換えたり、書籍の説明が自分には足りないと感じた部分を補完したりしています。書籍に忠実ではありません。

equalsメソッドはインスタンスAとインスタンスBが等しいかどうかを判定するためのメソッドで、その「等しい」の定義を「同一のインスタンスである」としたくない場合はequalsメソッドをオーバーライドしたほうが良い。値を保持するようなクラスはオーバーライドした方が良い場合が多い。
以下に具体例。

java.lang.Objectクラスのequalsの実装

public boolean equals(Object obj) {
    return (this == obj);
}

インスタンスにおける==演算子は左辺と右辺が同一インスタンスであるかどうかの判定をする。

java.lang.Stringクラスのequalsの実装

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

Stringクラスは値(文字列)を保持するクラスで、同一インスタンスでなくとも保持している値が等しければ「等しい」と判定するのが自然なので、Objectクラスから継承したequalsメソッドをオーバーライドしている。

equalsをオーバーライドするときの注意点

以下のURLにあるように
http://docs.oracle.com/javase/jp/7/api/java/lang/Object.html#equals(java.lang.Object)
equalsメソッドは5つの条件を満たすように実装されるべきである。それぞれ順を追って解説する。

反射性 (reflexive): null 以外の参照値 x について、x.equals(x) は true を返します。

自身のインスタンスを引数に渡したときに、trueを返すように実装するべき。これを満たさない実装となってしまうケースは考えにくい。

対称性 (symmetric): null 以外の参照値 x および y について、y.equals(x) が true を返す場合に限り、x.equals(y) は true を返します。

具象クラスを継承したときにこの条件を満たさないミスが起きやすい。

public class A {
    private int x;
    public A(int x){ this.x = x; }
    @Override public boolean equals(Object o){
        if(!(o instanceof A)) return false;
        A a = (A) o;
        return x == a.x;
    }
}

public class B extends A {
    private int y;
    public B(int x, int y) { super(x); this.y = y; }
    @Override public boolean equals(Object o) {
        if(!(o instanceof B)) return false;
        B b = (B) o;
        return super.equals(b) && y == b.y;
    }
}

ここで

A a = new A(1);
B b = new B(1,2);

a.equals(b); // ==> true
b.equals(a); // ==> false

となり、対称性を満たしていない。
以下に続く

推移性 (transitive): null 以外の参照値 x、y、および z について、x.equals(y) が true を返し、y.equals(z) が true を返す場合、x.equals(z) は true を返します。

対称性を確保するためにクラスBを以下のようにしたとする

public class B extends A {
    private int y;
    public B(int x, int y) { super(x); this.y = y; }
    @Override public boolean equals(Object o) {
        if(!(o instanceof A)) return false;           // 実装がA又はBでないインスタンスにはfalseを返す
        if(!(o instanceof B)) return super.equals(o); // 実装がAのインスタンスは、Aのequalsを使う
        B b = (B) o;
        return super.equals(o) && y == b.y;           // 実装がBのインスタンスの場合はこうする
    }
}

すると

A a = new A(1);
B b1 = new B(1,2);
B b2 = new B(1,3);

b1.equals(a);  // ==> true
a.equals(b2);  // ==> true
b1.equals(b2); // ==> false

となり、推移性を満たしていない。また、public class C extends A {...}があったとして、Cのインスタンスを渡されたときにクラスAのequalsを使うことが意味的に十分ではないケースも考えられる。
これはコンポジションを用いることで解決できる。

public class B {
    private A a;
    private int y;
    public B(int x, int y) { a = new A(x); this.y = y; }
    @Override public boolean equals(Object o) {
        if(!(o instanceof B)) return false;
        B b = (B) o;
        return a.equals(b.a) && y == b.y;
    }
    public A getA() { return a; }
}
A a = new A(1);
B b1 = new B(1,2);
B b2 = new B(1,3);

b1.equals(a);  // ==> false
a.equals(b2);  // ==> false
b1.equals(b2); // ==> false

b1.getA().equals(a);         // ==> true
a.equals(b2.getA());         // ==> true
b1.getA().equals(b2.getA()); // ==> true

一貫性 (consistent): null 以外の参照値 x および y について、x.equals(y) の複数の呼び出しは、このオブジェクトに対する equals による比較で使われた情報が変更されていなければ、一貫して true を返すか、一貫して false を返します。

意図せずに変わる値によって、equalsの結果が変わることは避けるべきである。これはequalsメソッドにおいて、信頼できない値を比較に用いないようにすればよい。信頼できない値とは例えば呼び出す度に外的要因によってその値が変わるものなどのこと。

null 以外の参照値 x について、x.equals(null) は false を返します。

これを満たさない実装となるケースは考えにくい。instanceof演算子は左辺にnullオブジェクトが来たときはfalseを返す仕様なので、instanceofを使っていれば間違いない。

ユニットテストを書く

上記5つの条件を守るためにはユニットテストを書いて確認すること。
ユニットテストの内容はクラスの構成、コンポジションの内容などによって変わるが、少なくとも間違えやすい対称性・推移性・一貫性の確認をするテストは書いたほうが良い。


最後に、間違えやすいのが

public boolean equals(HogeClass hogeInstance){
    //比較処理
}

引数の型がObjectではないこれはオーバーライドではなくオーバーロード。オーバーライドするメソッドには@Overrideアノテーションをつけることで、オーバーライドできていない時はコンパイル時にエラーを起こせるので間違いにすぐ気付くことが可能。