効率的Java技巧でequals方法を書き直す時、hashCode方法を書き換えるべきです.

5425 ワード

回転:http://tantanit.com/java-always-override-hashcode-when-override-equals/
hashCodeが満たすべき条件
  • equals方法に関するパラメータが変化していない場合、hashCodeは不変のままであるべきです.
  • もしequals方式によれば、2つのオブジェクトが等しくなるなら、この2つのオブジェクトのhashCodeは同じ
  • であるべきです.
  • 二つのオブジェクトが同じでないと、hashCodeは強制的に要求されないのとは違っていますが、保証があれば、ハッシュの効率が比較的に良くなります.
    最も重要なのは第二の点で、等しいオブジェクトは同じhashCodeが必要です.デフォルトのhashCode方法は各オブジェクトに対して固定の乱数値を返します.(ある実装は対象アドレスによって値を返します.各オブジェクトに対して固定の乱数値に相当します.)ので、equals方法を使うと同時に、override(書き換え)hashCode方法が必要です.これを満足させる.
    public class TantanitReaderPhone {
        private String areaCode;
        private String localNumber;
    
        public TantanitReaderPhone(String areaCode, String localNumber) {
            this.areaCode = areaCode;
            this.localNumber = localNumber;
        }
    
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    
        @Override
        public boolean equals(Object obj) {
            if (obj == this)  return true;
            if (!(obj instanceof TantanitReaderPhone))
                return false;
            TantanitReaderPhone tantanitReaderPhone = (TantanitReaderPhone)obj;
            return areaCode.equals(tantanitReaderPhone.areaCode)
                    && localNumber.equals(tantanitReaderPhone.localNumber);
        }
    
        public static void main(String[] args) {
            Map tantanitReaderPhoneStringMap
              = new HashMap<>();
            tantanitReaderPhoneStringMap.put(
                    new TantanitReaderPhone("86","13200001234"),"  "
            );
            String name=tantanitReaderPhoneStringMap.get(
                    new TantanitReaderPhone("86","13200001234")
            );
            if(name==null){
                System.out.print("name is null");
            }else {
                System.out.print(name);
            }
    
        }
    
    }
    上のコードは携帯電話番号の例です.携帯番号は区番号(例えば中国は86)と自国の携帯番号から構成されています.equals法を書き直したが、hashCodeが使用しているのは依然として親類、すなわちObject類の方法であり、乱数であると理解できる.メーン関数では、TantanitReader PhoneをkeyとするhashMapを定義し、value値を保存して取り出すことを試みた.注意したいのは、私たちが保存して取り出す時には、2つの異なるオブジェクト(2回ともnewの新しいオブジェクト)を使っていますが、2つのオブジェクトは同じアレアCodeとlocalNumberを持っています.私たちが書き換えたequals方法によって、この2つのオブジェクトは同じです.しかし、私たちはhashCodeを書き換える方法がないので、この2つのオブジェクトのハッシュ値は違っていますので、2番目のオブジェクトを使ってhashMapの中で最初に保存した値を見つけることができません.
    これは、ハッシュ・テーブルの各パーティションは、限られたハッシュ値にのみ対応し、これらのハッシュ値に対応するオブジェクトを格納するからである.したがって、ハッシュ値が違っていて、まず対応するパーティションが見つからないので、たとえ偶然にハッシュ・テーブルがパーティションを持っていても、同時に2つのハッシュ値に対応しているとしても、ハッシュ・テーブルは最適化されがちで、ハッシュ値を先に判断してしまうので、同じではないハッシュ値は対応するオブジェクトを見つけることができない.
    したがって、ハッシュ値に従って格納する必要がある場合、hashCode方法は、フィールド値に応じて対応するhashCode(ハッシュ値)を生成するべきである.hash値の計算方法を説明します.上の例を書き換えて、hashCode方法を書き直して、下記のmain関数を実行します.どのような結果があるかを見てください.
    どのようにhashCodeを計算しますか?
  • は、1つのint型変数resultを生成し、17
  • のような値を初期化する.
  • は、クラスの各重要フィールド、すなわち対象の値に影響を与えるフィールド、つまりequals方法に比較フィールドがあり、a.このフィールドの値filedHashValue bを計算し、relt=31*result+filedHashValueを実行する.更新結果
  • このフィールドの値filedHash Value値を計算すると、フィールドの種類によって3つの場合があります.一つは基礎データ(例えばint、bollan)で、一つは対象で、もう一つは配列です.
    重要なフィールドごとの値はどう計算しますか?
    フィールドがベースデータの場合、値はfとし、種類に応じて
  • bootlean、trueは1を返し、falseは0
  • を返します.
  • byte、char、shart、intは対応するint値
  • を返します.
  • longは、f^(f>>32)
  • に戻る.
  • floatは、Float.float ToIntBits(f)
  • に戻る.
  • doubleは、Double.double ToLongBits(f)をlongにして、compute(int)(f^(f>>32)に戻ります.
  • フィールドがオブジェクトである場合、nullであれば、0を返します.そうでなければ、上記の「hashCodeの計算方法」のステップ1と2に基づいて、このオブジェクトのhashCodeをこのフィールドの値として計算します.実は再帰の過程です.
    フィールドが配列であれば、各要素を計算し、filedHashValueを得て、reult=31*result+filedHashValueを実行する.
    ここではハshMapオブジェクトはどのようにハッシュ値を計算しますか?JDKでは、HashMap類自体がハシュコードを実現しました.一つのhashMapは実はentryの行列です.AbstractMapでは、hashCodeは、配列内の各entryに対してハッシュ値を計算し、すべてのハッシュ値の和を得る.
    public int hashCode() {
        int h = 0;
        Iterator> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode();
        return h;
    }
    各イベントは、以下のようにハッシュ値(JDK 1.7以降のバージョン)を計算する.
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    この実装は非常に巧妙であり、entryはkeyとvalueによって生成されるので、2つのハッシュ値を求め、同じkeyとvalueを持つentryが必ず同じハッシュ値を持つことを保証しつつ、効率を保証している.
    hashCodeの計算に関する注意事項
  • は、equals方法にないフィールドを含んではいけません.そうでないと、等しいオブジェクトが異なるハッシュ値
  • を有する可能性があります.
  • は、equals方法と同様に、他のフィールドから計算できるフィールド
  • を含むべきではない.
  • 計算の重要なフィールドを減らすことを試みないでください.hash値を計算する時は比較的早いですが、同じhash値が複数のオブジェクトに対応することになります.例えばTantanit Reader Phoneの中でaraCodeフィールドだけを使ってハッシュ値を計算すると、中国からの携帯電話番号はすべて同じhash値になります.明らかにハッシュテーブルの性能は低いです.
  • オブジェクトの値が変更できない場合、遅延計算を考慮して、例えばJDKのStringの値は可変ではない(毎回の変更値は新しいStringオブジェクトを生成する)ので、hash値の計算に遅延計算を使用し、キャッシュし、StringのhashCode方法は以下の通りである.
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
    
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    Tantanit Reader Phoneのハッシュ値計算
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode.hashCode();
        result = 31 * result + localNumber.hashCode();
        return result;
    }
    araCodeとlocalNumberはいずれもTantanit Reader Phoneを区別するための重要なフィールドであるため、この2つのフィールドからハッシュ値を計算する.この二つのフィールドはStringタイプで、Stringが所有するhashCodeメソッドを直接呼び出します.
    Tantanit Reader PhoneのhashCode方法を上記のコードに従って書き直した後、main関数を実行して、「張三」を印刷し、hashMapから対応する値を取り出すことに成功しました.Tantanit Reader Phoneのコードをコピーして、自分で試してみてもいいです.