前回に引き続き、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