The HIRO Says

If you smell what The HIRO is cooking!!!

equals() メソッドの実装(3)−transitivity の実現と継承

前回に引き続き、equals() メソッドの規約の1つ、transitivity について触れていこうと思います。


transitivity の意味

transitivity とは、a.equals(b) = true かつ b.equals(c) = true ならば c.equals(a) = true である、という意味です。
前回の symmetry を、更に拡張したような意味合いですね。


実装方法

通常、equals() メソッドの実装(1)−基本的な規約と同様の実装をすれば、特別問題は発生しません。
但し、クラスを継承で拡張した場合は、色々と面倒なことが起こります。
以下、継承をした場合の問題と対策について説明していきます。


継承で起こる問題

前回のequals() メソッドの実装(2)−symmetry の実現で使用した Title クラスを拡張し、色(java.awt.Color)を保持できるようにしてみましょう。

public class TitleWithColor extends Title {

    private Color color;

    public Color getColor() {
        return this.color;
    }

    public void setColor(Color color) {
        this.color = color;
    }
}


この場合、equals() メソッドを以下のように実装したらどうなるでしょうか?

    @Override
    public boolean equals(Object o) {

        if (o == this) {
            return true;
        }

        if (!(o instanceof Title)) {
            return false;
        }

        // 型が Title の場合、name のみを比較
        if (!(o instanceof TitleWithColor)) {
            return super.equals(o);
        }

        // 型が TitleWithColor の場合
        TitleWithColor target = (TitleWithColor) o;

        // name と color を比較
        return super.equals(o) &&
                (this.color == null ? target.color == null : this.color.equals(target.color));
    }


実行結果は次のようになり、transitivity に違反することになります。
(true - true -true であるべきなのに、true - true - false となってしまいます。)

        TitleWithColor target1 = new TitleWithColor();
        Title          target2 = new Title();
        TitleWithColor target3 = new TitleWithColor();

        target1.setName("A");
        target1.setColor(Color.RED);

        target2.setName("A");

        target3.setName("A");
        target3.setColor(Color.BLUE);
     ↓
        target1.equals(target2) : true
        target2.equals(target3) : true
        target3.equals(target1) : false

継承で起こる問題の解決方法

継承ではなく、親に当たるクラスをインスタンス変数として保持すれば、transitivity を実現することができます。
但しこの場合、継承関係はなくなるので注意。

public class TitleWithColor {

    private Title title;
    private Color color;

    @Override
    public boolean equals(Object o) {

        // reflexivity の検証(パフォーマンス向上策を含む)
        if (o == this) {
            return true;
        }

        // non-nullity と型の検証
        if (!(o instanceof TitleWithColor)) {
            return false;
        }

        TitleWithColor target = (TitleWithColor) o;

        // symmetry の検証
        return *1 &&
                (this.color == null ? target.color == null : this.color.equals(target.color)));
    }

// getter/setter は省略
}

*1:this.title == null ? target.title == null : this.title.equals(target.title