equals一般的な約束を遵守し、再定義


equalsは誤って再定義すると恐ろしい結果をもたらす。したがって、次のいずれかに該当する場合は、再定義しないほうがよい。

  • の各例は本質的に固有である.ここには、値ではなく、アクションオブジェクトを表すクラスが含まれています.Threadは良い例です.
  • インスタンスの「論理的等価性」をチェックする必要はありません.
  • 上流クラスで再定義されたequalsも下流クラスに適している.例えば、Listインプリメンテーションは、AbstractListから継承されて使用される.
  • クラスはprivateまたはpackage-privateであり、equalsメソッドを呼び出す必要はありません.
  • equalsが呼び出されるのを阻止したくない場合は、次のように実現します.
    @Override
    public boolean equals(Object a){
        throw new AssertionError(); 호출 금지
    }

    では、equalsを再定義するのはいつですか?


    論理整合性ではなく、オブジェクト識別性(objectidentity:2つのオブジェクトが物理的に同じかどうかを確認する必要があります.親クラスのequalsを再定義して論理整合性を比較しない場合.主に価値のある類です.値クラスとは、IntegerやStringのように値を表すクラスです.

    equalsメソッドを再定義する場合は、一般的な約束に従う必要があります。


    equalsメソッドは等値関係(equals relation)を実現し、以下の条件を満たす.
    →同治関係って何?簡単に言えば、集合を同じ要素からなる部分集合に分ける演算である.この一致関係を満たすために、次は5つの要求を1つずつ見てみましょう.
  • 反射性:非空のすべての参照値xに対して、x.equals(x)はtrueである.
  • →相手は自分と同じだという意味です.故意にこの条件に違反しなければ、満足しにくい.
  • 対称性(対称性):空でないすべての参照値x,y,x.equals(y)はtrue,y.equals(x)もtrueである.
  • →これは、2つのオブジェクトが同じように一致しているかどうかを答えなければならないことを意味します.
    import java.util.Objects;
    
    public class CaseInsesitiveString {
        private final String s;
    
        public CaseInsesitiveString(String s) {
            this.s = Objects.requireNonNull(s);
        }
    
        @Override
        public boolean equals(Object o) {
            if(o instanceof CaseInsesitiveString)
                return s.equalsIgnoreCase(
                        ((CaseInsesitiveString) o).s);
            if(o instanceof String)
                return s.equalsIgnoreCase((String) o);
            return false;
        }
    }
    public class Main {
        public static void main(String[] args) {
            CaseInsesitiveString cis = new CaseInsesitiveString("Hello");
            String s = "hello";
    
            System.out.println(s.equals(cis));
            System.out.println(cis.equals(s));
        }
    }
    
    실행결과
    false
    true
    このコードの問題は,CaseInsensitiveStringのequalsは通常Stringを知っているが,StringのequalsはCaseInsensitiveStringの存在を知らないことである.従って,s.equals(cis)はfalseを返し,対称性に著しく違反した.
    この問題を解決するには、CaseInsensitiveStringのequalsとStringを結びつけるでたらめな夢を放棄しなければならない.
            if(o instanceof String)
                return s.equalsIgnoreCase((String) o);
            return false;
    したがって、CaseInsistiveStringクラスのequalsメソッドから上記の部分を削除する必要があります.
  • 繰返し性(伝達性):null以外のすべての参照値x,y,zに対して、x.equals(y)はtrue、y.equals(z)はtrueであり、x.equals(y)はtrueである.
  • →1番目のオブジェクトと2番目のオブジェクトが同じで、2番目のオブジェクトと3番目のオブジェクトが同じである場合、1番目のオブジェクトと3番目のオブジェクトも同じである必要があります.この要求も簡単だが、うっかりすると背きやすい.親にない新しいフィールドを子に追加する場合を考慮します.
    まず,矩形の横と縦を表すクラスを以下に示す.
    public class Point {
        private final int x;
        private final int y;
    
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Point))
                return false;
            Point p = (Square)o;
            return p.x == x && p.y == y;
        }
    }
    クラスを展開し、長方形に色を追加します.
    public class ColorPoint extends Point{
        private final Color color;
    
        public ColorPoint(int x, int y, Color color) {
            super(x, y);
            this.color = color;
        }
    }
    equalsメソッドをそのままにすると、Squareの実装が継承され、色情報を無視して比較が実行されます.
    以下のコードに示すように、比較対象が別のColorPointであり、位置と色が同じである場合にのみtrueのequalsが返されることを考慮してください.
    @Override
    public boolean equals(Object o){
        if(!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
    このメソッドはfalseを返すたびに、Squareのequalsは色を無視し、CoorsquareのequalsはSquareオブジェクトを受け入れないためです.
    では次のようにColorSquareequalsとSquareを比較する場合、色を無視して解決できますか?
    @Override
    public boolean equals(Object o){
        if(!(o instanceof ColorSquare))
            return false;
        if(!(o instanceof ColorSquare))
            return o.equals(this);
        return super.equals(o) && ((ColorSquare) o).color == color;
    }
    この方式は対称性を保っているが,同性化を破った.
    public static void main(String[] args) {
        ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
    
        System.out.println(p1.equals(p2));
        System.out.println(p2.equals(p3));
        System.out.println(p1.equals(p3));
    }
    
    실행결과
    true
    true
    false
    では、解決策は何でしょうか.これはすべてのオブジェクト向け言語の同治関係に現れる根植的な問題である.拡張特定のクラスに新しい値を追加する場合、equals約定を満たす方法は存在しません.オブジェクト向けの抽象化のメリットを放棄しない限り.
    equalsのinstanceofチェックをgetClassチェックに変更すると、約束を守り、値を追加して特定のクラスを継承できるという意味です.
    ColorSquare.equalsメソッドを
    @Override
    public boolean equals(Object o){
        if(o == null || o.getClass() != this.getClass())
            return false;
        Point p = (Point) o;
        return p.getX() == this.getX() && p.getY() == this.getY();
    }
    実行
    public static void main(String[] args) {
        ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
    
        System.out.println(p1.equals(p2));
        System.out.println(p2.equals(p3));
        System.out.println(p1.equals(p3));
    }
    실행 결과
    false
    true
    true
    今回は、同じインプリメンテーションクラスのオブジェクトと比較した場合にのみtrueを返します.
    getClassを使用する場合、リスク切替の原則(=任意のタイプにとって、重要な属性はそのサブタイプでも重要です).背くことができる.すなわち,Pointのサブクラスは定義上は依然としてPointであるため,どこでもPointとして使用すべきである.(したがって、継承関係はinstanceofを使用する必要があります.)
    例えば、与えられた点が単位円内(半径1)であるかどうかを決定する方法が必要であると仮定する.
    	
    public class Point {
        ...
        ...
     	  private static final Set<Point> unitCircle = Set.of(
    		        new Point(1, 0), new Point(0, 1),
    		        new Point(-1, 0), new Point(0, -1)
    		);
    		
    		public static boolean onUnitCircle(Point p) {
    		    return unitCircle.contains(p);
    		}
    }
    CounterPoint
    public class CounterPoint extends Point {
        private static final AtomicInteger counter = new AtomicInteger();
    
        public CounterPoint(int x, int y) {
            super(x, y);
            counter.incrementAndGet();
        }
    
        public static int numberCreated() {
            return counter.get();
        }
    }
    public class Main {
        public static void main(String[] args) {
            Point p = new Point(1, 0);
            CounterPoint.onUnitCircle(p);
            System.out.println(CounterPoint.numberCreated());
    
            Point cp = new CounterPoint(1, 0);
            CounterPoint.onUnitCircle(cp);
            System.out.println(CounterPoint.numberCreated());
        }
    }
    실행결과
    0
    1
    特定のクラスのサブクラスに値を追加することはできませんが、良い迂回方法があります.「継承の代わりに複合語を使用する」項目18の提案に従えばよい.ポイントを継承する代わりにColorPointのprivateフィールドとして使用し、ColorPointなどのビューメソッドをpublicに追加します.
  • 一貫性:null以外のすべての参照値x,yに対して、x.equals(y)を繰り返し呼び出すと、常にtrueまたはfalseが返されます.
  • →コンシステンシとは、2つのオブジェクトが同じである場合(いずれかまたは両方が変更されていない場合)、以降も常に同じであることを意味します.可変オブジェクトは、異なる比較点で異なる場合もあれば、同じ場合もありますが、不変オブジェクトが異なると、常に異なる必要があります.
    クラスが不変であろうと可変であろうと,equalsの判断に信頼できないリソースを介入させることはできない.この制約に違反すると,コンシステンシ条件を満たすことは困難である.たとえばjavaです.net.URLのequalsは,所与のURLとマッピングホストのIPアドレスを用いて比較する.ホスト名をIPアドレスに変更するには、ネットワークを通過する必要がありますが、結果が常に同じであることは保証されません.これによりURLのequalsは一般的な約束に違反し,実際の作業でも問題を引き起こすことが多い.
    このような問題を回避するには、equalsはメモリに存在するオブジェクトのみを使用して決定的な計算を行うことができます.
  • null-No:null以外のすべての参照値xの場合、x.equals(null)はfalseです.
  • →null-または、すべてのオブジェクトがnullと同じ名前にならないことを示します.
    o.equals(null)がtrueを返すことは想像しにくいが,nullPointerExceptionのコードをうっかり投げ出すのはよくある.この一般的なルールはこの場合も許されない.多くのクラスはnullと入力して自分を保護します.
    public boolean equals(Object o){
        if(o == null){
            return false
        }
        ...
    }
    この検査は必要ありません.instanceof(2番目の被演算子と罪のない)最初の被演算子がnullの場合falseが返されます.