[Java]equalsメソッドと継承

2016年5月16日月曜日

Java

Javaのequalsの実装でふと迷ったことがあった。

以前にもequalsメソッドをオーバーライドするときにテストするべきことを書いたけど、 そのときは比較対象のオブジェクトが自分と同じクラスかどうかのチェックには触れなかった。

instanceof演算子で比較するか、getClassメソッドで比較するかで、以下のような問題があるみたい。
Stringのフィールドを持ったクラスのequalsメソッドを素直に実装すると以下のような感じ。
class Base {
    final String id;

    Base(String id) {
        this.id = id;
    }

    // instanceof演算子で比較する
    @Override public boolean equals(Object obj) {
        if (!(obj instanceof Base)) return false;

        return Base.equals((Base)obj, this);
    }

    private static boolean equals(Base a, Base b) {
        if (a == b) return true;

        return a.id.equals(b.id);
    }
}

けれども、instanceofではなくgetClassを使うやり方もある。
    // getClassメソッドで比較する
    @Override public boolean equals(Object obj) {
        if (obj == null || obj.getClass() != getClass()) return false;

        return Base.equals((Base)obj, this);
    }

ここまではいいとしても、このクラスを継承したクラスでもさらにequalsが実装されていたとしたらどうなるか。
class Extended extends Base {
    final String subId;

    Extended(String id, String subId) {
        super(id);
        this.subId = subId;
    }

    // instanceof演算子で比較する
    @Override public boolean equals(Object obj) {
        if (!(obj instanceof Extended)) return false;
        if (super.equals(obj)) return false;

        return Extended.equals((Extended)obj, this);
    }

    private static boolean equals(Extended a, Extended b) {
        if (a == b) return true;

        return a.subId.equals(b.subId);
    }
}

instanceofの場合、どちらのインスタンスメソッドを呼び出すかによって、結果が変わってしまう。
    Base base1 = new Base("123");
    Base extended1 = new Extended("123", "ABC");

    System.out.println(base1.equals(extended1)); // true
    System.out.println(extended1.equals(base1)); // false

これは対称律が成り立っていないと言えそうだ。
では、getClassではどうか?
    // getClassの場合のExtendedクラスのequalsメソッド
    @Override public boolean equals(Object obj) {
        if (obj == null || obj.getClass() != getClass()) return false;
        if (super.equals(obj)) return false;

        return Extended.equals((Extended)obj, this);
    }
    Base base1 = new Base("123");
    Base extended1 = new Extended("123", "ABC");

    System.out.println(base1.equals(extended1)); // false
    System.out.println(extended1.equals(base1)); // false

今度は対称律は満たすけど、両方ともfalseになってしまった。

でも、これはこれで問題がある。
コレクションに入っているスーパークラスのインスタンスをサブクラスをキー指定して取り出すことができなくなってしまうのだ。
    Base base1 = new Base("123");
    Base extended1 = new Extended("123", "ABC");

    HashSet<Base> hashset = new HashSet<>();
    hashset.add(base1);
    System.out.println(hashset.contains(extended1)); // false


どちらの挙動のほうが正しいと考えるかで、instanceof派とgetClass派がいるみたい。
どちらにしても完全に納得のいく形にすることはできなさそう。。。

考え方としてはフィールドを増やすことによって、スーパークラスでは同一インスタンスとみなされるものが異なるインスタンスとみなされるようなクラス設計がおかしいというのもあるかもしれない。

参考:
http://stackoverflow.com/questions/596462/any-reason-to-prefer-getclass-over-instanceof-when-generating-equals