[Java]すべてのオブジェクトの共通メソッド2


これはEFFECTIVE JAVA 3/E書の学習と整理の文章です.

📖 equalsを再定義する場合はhashCodeを再定義します


📌 hashCode一般規則


すべての再定義
  • equalsのクラスでは、hashCodeも再定義する必要があります.
    --それ以外の場合、hashCodeの一般的な規則に違反します.
    -->クラスのインスタンスをセット内の要素(たとえばHashMapやHashSet)として使用すると、問題が発生します.
  • に約束equals比較に使用する情報が変更されていない場合は、アプリケーションの実行中にオブジェクトのhashCodeメソッドが何度呼び出されても、常に同じ値を返さなければなりません.ただし、アプリケーションを再実行すると、この値は変更されます.
    に約束equals(Object)が2つのオブジェクトが同じと判断した場合、2つのオブジェクトのhashCodeは同じ値を返さなければなりません.
    に約束equalsが2つのオブジェクトが異なると判断しても,2つのオブジェクトのhashCodeは異なる値を返す必要はない.ただし、ハッシュ・テーブルのパフォーマンスを向上させるには、他のオブジェクトに異なる値を返さなければなりません.

  • hashCodeを再定義する際に深刻な問題が発生した2番目の条項です.すなわち、論理的には、同じオブジェクトは同じハッシュコードを返さなければならない.

  • equalsは物理的に異なる2つのオブジェクトを論理的に同じと言える.
    --ただし、ObjectのデフォルトhashCodeメソッドでは両者が全く異なると考えられているため、返される値は約定とは異なります.
  • --前章で使用したPhoneNumberクラスを例として使用します.
    // [코드 10-6] 전형적인 equals 메서드의 예
    
    public final class PhoneNumber {
    	private final short areaCode, prefix, lineNum;
        
        public PhoneNumber(int areaCode, int prefix, int lineNum) {
        	this.areaCode = rangeCheck(areaCode, 999, "지역코드");
            this.prefix = rangeCheck(prefix, 999, "프리픽스");
            this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
        }
        
        private static short rangeCheck(int val, int max, String arg) {
        	if(val < 0 || val > max)
            	throw new IllegalArgumentException(arg + ": " + val)
                
          	return (short)val;
        }
        
        @Override public boolean equals(Object o) {
        	if(o == this)
            	return true;
                
          	if(!(o instanceof PhoneNumber))
            	return false;
                
         	PhoneNumber pn = (PhoneNumber)o;
            
            return pn.lineNum == lineNum && pn.prefix == prefix && 
            pn.areaCode == areaCode;
        }
        
        ... // 나머지 코드는 생략
    }
  • は、このコードクラスのインスタンスをHashMapの要素として使用すると仮定する.
  • Map<PhoneNumber, String> m = new HashMap<>();
    m.put(new PhoneNumber(707, 867, 5309), "제니");

  • このコードの後にm.get(新しいPhoneNumber(7078675309)が実行され、「ジェニーン」が現れる可能性がありますが、実際にはnullが返されます.
    --HashMapに「ジェニーン」を入れた場合、取り出しに2つのインスタンスが使用されます.

  • 上のPhoneNumberクラスはhashCodeを再定義していないため,2つの論理が同じオブジェクトは異なるハッシュコードを返し,約束2を遵守できない.
    -->その結果、getメソッドは予期せぬハッシュパケットでオブジェクトを検索しようとします.
    --同じハッシュ・パケットでオブジェクトを検索しても、getメソッドはnullを返します.これは、HashMapがハッシュ・コードを最適化し、異なるエントリ間での一貫性の比較を回避するためです.

  • 解決策:クラスに適切なhashCodeメソッドを作成するだけです.
  • // [코드 11-1] 최악의 (하지만 적법한) hashCode 구현 - 사용 금지!
    
    @Override public int hashCode() {
    	return 42;
    }

  • このコードは,すべての同治オブジェクトに同じハッシュコードを返すことで正当性を実現するが,すべてのオブジェクトの同じ値のみを返すため,実行時間O(1)のハッシュテーブルがO(n)に遅くなり,オブジェクト数が増加すると性能が低下する.

  • 良いハッシュ関数は、異なるハッシュコードを異なるインスタンスに返します.
    --hashCodeの3番目の約束
  • 📌 hashCodeを作成するための簡単なテクニック

  • int変数の結果を宣言し、値cに初期化する
    このとき、cは、オブジェクトの最初のキーフィールドをステップ2に設定する.a方式で計算されたハッシュコード(コアフィールド:equalsを比較するためのフィールド)
  • は、オブジェクトの残りのキーワードセグメントfに対して、以下の動作
  • を実行する.
    2−a.フィールドのハッシュコードcを計算する
  • 2-a-i.基本タイプフィールドの場合、タイプ.hashCode(f)を実行します.
    ここでType:デフォルトタイプのモザイククラス
  • 2-a-ii. 参照タイプフィールドでクラスのequalsメソッドがフィールドを再帰的に呼び出すequalsによって比較される場合、そのフィールドのhashCodeが再帰的に呼び出されます.
    計算がより複雑になると、フィールドの標準タイプ(典型的には)が作成され、その標準タイプのhashCodeが呼び出されます.
    フィールドの値がnullの場合、従来は0が使用されています.
  • 2-a-iiii.フィールドが配列である場合、各コア要素は個別のフィールドとして処理される.
    アレイにコア要素が1つもない場合は、定数(0を推奨)のみが使用されます.
    すべての要素がコア要素である場合、Arrays.hashCodeの使用
    ステップ2-b.ステップ2-aで算出したハッシュコードcを用いて結果を更新する
    result = 31 * result + c;
  • 結果
  • を返します.

  • 上記のテクニックを満たすためにhashCodeを実装した場合、このメソッドが同じhashCodeを返すかどうかを問い合せ、ユニットテストを作成できます.
    --フレームワークAutoValueを使用して作成する場合は、スキップできます.

  • 派生フィールドは、ハッシュコード計算から除外できます.
    --つまり、他のフィールドから計算できるすべてのフィールドを新しいものにすることができます.
    --さらに、equals比較に使用されないフィールドは「除外」する必要があります.
    --そうでなければ、hashCodeの約束第二条に違反する危険があります.

  • 手順2.bの乗数31*resultは、フィールドの乗数順に従って結果値を変更する.
    --したがって、クラスに複数の類似フィールドがある場合、ハッシュ効果が大幅に向上します.
    --StringのhashCodeに乗算が実装されていない場合、すべてのバイナリ(管理図、構成されたスペルが同じで、順序の異なる文字列のみ)のhashCodeは同じになります.
    --31:奇数で素数なので通常使います.
  • // [코드 11-2] 전형적인 hashCode 메서드
    
    @Override public int hashCode() {
    	int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
    }

  • このメソッドでは、PhoneNumberインスタンスの3つのコアフィールドのみを使用して単純な計算を行います.
    --このプロセスでは、不確実性要素は存在しないので、一貫したPhoneNumberインスタンスは同じハッシュコードを持つことになる.
    --javaプラットフォームライブラリ内のクラスが提供するhashCodeメソッドに比べて、この点は十分ではありません.
    --ただし、より少ないハッシュ競合を使用する必要がある場合は、Guarbaを使用します.

  • Objectsクラスは、任意の数のオブジェクトを受信し、ハッシュコードを計算する静的方法hashを提供する.
    --速度はやや遅いが簡単なコードを作成できます.
    --したがって、hashメソッドは、パフォーマンスに敏感でない場合にのみ使用されます.
  • // [코드 11-3] 한 줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다.
    
    @Override public int hashCode() {
    	return Objects.hash(lineNum, prefix, areaCode);
    }
  • クラスが不変であり、ハッシュコードを計算するコストが高い場合、毎回再計算するのではなく、キャッシュの方法を考慮する.
    --このタイプのオブジェクトが主にハッシュキーとして使用される場合は、インスタンスの作成時にハッシュコードを計算する必要があります.
    --ハッシュキーを使用しない場合は、遅延初期化(lazy initiation)を考慮することができます.逆に、スレッドのセキュリティに注意してください.
    --hashCodeフィールドの初期値は、通常生成されるオブジェクトのハッシュコードとは異なる必要があります.
  • // [코드 11-4] 해시 코드를 지연 초기화하는 hashCode 메서드 - 스레드 안전성까지 고려해야 함.
    
    private int hashCode; // 자동으로 0으로 초기화
    
    @Override public int hashCode() {
    	int result = hashCode;
        if(result == 0) {
        	result = Short.hashCode(areaCode);
            result = 31 * result + Short.hashCode(prefix);
            result = 31 * result + Short.hashCode(lineNum);
            hashCode = result;
        }
        return result;
    }

  • パフォーマンスを向上させながらハッシュコードを計算しようとすると、コアフィールドは省略できません.
    --高速ですが、ハッシュ・テーブルの品質が悪くなると、ハッシュ・テーブルのパフォーマンスが大幅に低下します.

  • hashCode戻り値の生成ルールはAPIユーザに詳細に公表しないでください.これにより、クライアントはこの値に依存せず、後で計算方法を変更する可能性があります.
    --詳細ルールを公表しない場合は、ハッシュ機能に欠陥を発見したり、より良いハッシュ方式を見つけたりして、次のバージョンで変更できます.
  • 📌 コアの整理


    equalsを再定義する場合、hashCodeも再定義する必要があります.
    再定義されたhashCodeは、一般的な規則に従う必要があります.異なるインスタンスであれば、できるだけ異なるhashCodeを実装する必要があります.
    AutoValueフレームワークまたはその他のIDEを使用すると、equalsおよびhashCodeが自動的に生成されます.