JAvaカスタムequals関数とhashCode関数

10494 ワード


すべてのクラスはObjectクラスから継承され、彼のすべての非finalメソッド:equals、hashCode、toString、clone、finalizeは、共通の約束を持っています.これらの方法を上書きするときは、これらの約束に従う必要があります.そうしないと、これらの約束に依存するクラス(例えば、HashMapおよびHashSet)は、クラスと組み合わせて動作しません.
 
一.equals
等しい概念:

  • 論理的に等しい:たとえばIntegerに含まれる数値が等しい場合、この2つのIntegerは等しいと考えます.例えば、AbstractListでは、2つのlistに含まれるすべての要素が等しい場合、2つのListが等しい.

  • 本当の意味での等しい:同じオブジェクトを指します.

  • equals関数を再ロードしない場合、2つのクラスの等しいことは本当の意味でのequalにすぎません.クラスが自分の等しい論理を望む場合はInteger/Listのようにequals関数を再ロードする必要があります.
     
    JAva仕様におけるequalsメソッドの特徴

  • 自己反転性:非空参照xの場合、x.equals(x)はtrueを返します.

  • 対称性:任意の参照x、yに対して、y.equals(x)がtrueを返す場合のみ、x.equals(y)がtrueを返す場合.

  • 伝達性:任意の参照x,y,zに対して、x.equals(y)がtrueを返すと、y.equals(z)がtrueを返す.x.equals(z)はtrueを返します.

  • コンシステンシ:xとyが参照するオブジェクトが変更されていない場合、x.equals(y)を繰り返し呼び出すと同じ結果が返される.

  • 任意の非空参照xに対して、x.equals(null)はfalseを返す.

  • この問題は2つの異なる状況で見ることができます.

  • サブクラスが独自の等しい概念を持つことができる場合、対称性需要はgetClassを強制的に採用して検出する.

  • スーパークラスによって等しい概念が決定されるとinstanceofで検出され,サブクラスを用いないオブジェクト間で等しい比較が可能となる.

  •  
    TimeStampの非対称性
    Date date = new Date();
    Timestamp t1 = new Timestamp(date.getTime());
    
    System.out.println("Date equals Timestamp ? : " +  date.equals(t1));// true
    System.out.println("Timestamp equals Date ? : " +  t1.equals(date));// false

     
    TimeStampソース:(getClass()ではなくinstanceofを使用)
        // Timestamp
        @Override
        public boolean equals(java.lang.Object ts) {
            if (ts instanceof Timestamp) {
                return this.equals((Timestamp)ts);
            } else {
                return false;//  Timestamp       false
            }
        }
        //       
        public boolean equals(Timestamp ts) {
            if (super.equals(ts)) {
                if  (nanos == ts.nanos) {
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }
    

    親Date:
        // Date
        @Override
        public boolean equals(Object obj) {
            return obj instanceof Date && getTime() == ((Date) obj).getTime();
        }
    

     
    コメント:
  • 標準のjavaライブラリには150以上のequalsメソッドの実装が含まれており、instanceof検出、getClass検出の呼び出し、ClassCastException検出のキャプチャ、または何もしない.java.sql.TimeStamp実装者は、TimestampクラスはDateクラスを継承し、後者のequalsメソッドはinstanceof検出を使用し、equalsメソッドを書き換える場合、対称性は同時に達成できない.
  • スーパークラスによって等しいと決定された場合、finalキーワード修正比較関数を考慮することができ、サブクラスequalsメソッドの柔軟性を考慮すれば、AbstractSet.equalsメソッドのような修飾を加えることなくfinalと明記すべきであり、これによりサブクラスHashSetとTreeSetを比較することができるが、サブクラスの柔軟性を考慮して、何の修飾も追加しない.
     
    equalsメソッドの推奨事項を記述します.
  • 表示パラメータはotherObjectと命名され、後でother変数public boolean equals(Object otherObject)
  • に変換されます.
  • thisとotherObjectが同じオブジェクトの参照であるかどうかを検出します.はい、trueを返します.if(this==otherObject){return true;}
  • otherObjectがnullであるかどうかを検出し、はい、falseを返します.if(otherObject == null){return false;}
  • thisとotherObjectが同じクラスに属しているかどうかを比較します.equalsの意味が各サブクラスで変更された場合、getClassを使用してif(getClass()!=otherObject.getClass(){return false;}サブクラスの意味が同じ場合、instanceof検出:if(!(otherObject instanceof Employee){return false;}を使用します. 
  • otherObjectを対応するタイプ変数other Employee other=(Employee)otherObjectに変換します.
  • 必要な比較データドメインを比較する.基本データ型であればa=bを用いて比較する.オブジェクト比較の場合は、Objects.equals(a,b)を呼び出してreturn Objects.equals(name,other.name)&&salary==other.salary&&Objects.equals(hireDay,other.hireDay);

  •  
    二、hashCode()
    設計の原則には、equalsを上書きするときはhashCodeを上書きすることがあります.
    hashCode符号化の原則:
    1.オブジェクトequalsメソッドの比較操作に使用される情報が変更されていない限り、hashCodeメソッドは同じ整数を返す必要があります.同じアプリケーションの複数回の実行中に、実行ごとに返される整数が一致しなくてもよい.
    2.equals(Object)メソッドに従って2つのオブジェクトが等しい場合、hashCodeの戻り値は同じです.
    3.両オブジェクトがequals(Object)メソッドに従って比較が異なる場合、両オブジェクトのhashCode戻り値は必ずしも等しくないとは限らないが、異なるオブジェクトに異なる整数結果を生じさせ、ハッシュ・リストの性能を向上させることができる.
    具体例
    クラスがequalsを上書きしてequals関数を上書きしているのにhashCodeを上書きしていない場合は、上記の第2の原則に違反します.次にhashCodeをリロードしていない例を見てみましょう.
    public class PhoneNumber {
        private final int areaCode;
        private final int prefix;
        private final int lineNumber;
    
        public PhoneNumber(int areaCode, int prefix, int lineNumber) {
            rangeCheck(areaCode, 999, "area code");
            rangeCheck(prefix, 999, "prefix");
            rangeCheck(lineNumber, 9999, "line number");
            this.areaCode = areaCode;
            this.prefix = prefix;
            this.lineNumber = lineNumber;
        }
    
        private static void rangeCheck(int arg, int max, String name) {
            if(arg < 0 || arg > max) {
                throw new IllegalArgumentException(name + ": " + arg);
    
            }
        }
    
        @Override
        public boolean equals(Object o) {
            if(o == this)
                return true;
            if(!(o instanceof PhoneNumber))
                return false;
            PhoneNumber pn = (PhoneNumber)o;
            return pn.lineNumber == lineNumber
                    && pn.prefix == prefix
                    && pn.areaCode == areaCode;
        }
    
    }

    次のコードを実行します.
    Map map = new HashMap();
    map.put(new PhoneNumber(707, 867, 5309), "Jenny");
    System.out.println(map.get(new PhoneNumber(707, 867, 5309)));

    Jennyに戻ることを期待していますがnullを返します.
    なぜならhashCodeの約束に違反し、PhoneNumberがhashCodeメソッドをカバーしていないため、2つの等しいインスタンスが等しくないハッシュコードを有し、putメソッドは電話番号オブジェクトを1つのハッシュバケツに配置し、getメソッドは別のハッシュバケツからこの電話番号の所有者を検索し、明らかに見つからないからである.
    hashCodeをカバーして約束を守れば、この問題を修正することができます.
     
    良いハッシュ関数は「等しくないオブジェクトに等しくないハッシュコードを生成する」傾向があり、以下に簡単な解決策があります.
    1.resultというintタイプの変数に、17のようなゼロでない定数値を保存します.(2.aで計算されるハッシュ値が0の初期ドメインのためにハッシュ値に影響します)
    2.オブジェクト内の各キードメインfについて、次の手順を完了する.
    a.このドメインのintタイプのハッシュコードcを計算する
    i.ドメインがbooleanの場合、計算(f?1:0)
    ii.ドメインがbyte、char、shortまたはintタイプである場合、計算(int)f
    iii.ドメインがlongである場合、計算(int)(f^(f>>>32))
    iv.ドメインがfloatである場合、Float.floatToIntBits(f)を計算する
    v.ドメインがdoubleの場合、Double.doubleToLongBits(f)を計算し、
      vi.ドメインがオブジェクト参照であり、クラスのequalsメソッドがequalsを再帰的に呼び出すことによってこのドメインを比較する場合、hashCodeは同様にこのドメインに再帰的に呼び出される.より複雑な比較が必要な場合は、このドメインの「パターン」を計算し、この「パターン」に対してhashCodeを呼び出します.ドメインの値がnullの場合、0(または通常は0)が返されます.
    vii.ドメインが配列である場合、各要素を個別のドメインとして処理する、すなわち、上記のルールを再帰的に適用し、重要な要素ごとにハッシュコードを計算し、2.bに基づいてこれらのハッシュ値を組み合わせる.配列ドメインの各要素が重要である場合、1.5で追加されたArray.hashCodeメソッドの1つを使用できます.
    b.次の式に従って、ステップ2.aで計算したハッシュコードcをresultにマージする.
      result = 31 * result + c.(31を選択したのは奇素数であり、乗数が偶数の場合、乗算オーバーフロー時に情報が失われるため、VMは31*i=(i<<5)-iを最適化できる)
    3.resultを返します.
    hashCodeメソッドを記述した後、記述ユニットテストは、同じインスタンスに等しいハッシュコードがあるかどうかを検証する.
    上記の解決方法をPhoneNumberクラスに適用します.
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

    今までのテストコードを使って、Jennyに戻ることができることを発見しました.