web-dev-qa-db-ja.com

HashMapのequalsとhashCodeの仕組みを理解する

私はこのテストコードを持っています:

_import Java.util.*;

class MapEQ {

  public static void main(String[] args) {
   Map<ToDos, String> m = new HashMap<ToDos, String>();
   ToDos t1 = new ToDos("Monday");
   ToDos t2 = new ToDos("Monday");
   ToDos t3 = new ToDos("Tuesday");
   m.put(t1, "doLaundry");
   m.put(t2, "payBills");
   m.put(t3, "cleanAttic");
   System.out.println(m.size());
} }

class ToDos{

  String day;

  ToDos(String d) { day = d; }

  public boolean equals(Object o) {
      return ((ToDos)o).day == this.day;
 }

// public int hashCode() { return 9; }
}
_

// public int hashCode() { return 9; }がコメント化されていない場合、m.size()は2を返し、コメントが残っている場合は3を返します。どうして?

42
andandandand

HashMapは、エントリの検索にhashCode()==およびequals()を使用します。特定のキーkのルックアップシーケンスは次のとおりです。

  • k.hashCode()を使用して、エントリが保存されているバケットがあれば、それを決定します
  • 見つかった場合、そのバケット内の各エントリのキーk1に対して、k == k1 || k.equals(k1)の場合、k1のエントリを返します
  • その他の結果、対応するエントリなし

例を使用してデモンストレーションするために、HashMapクラスで表される同じ整数値を持つキーが「論理的に同等」であるAmbiguousIntegerを作成すると仮定します。次に、HashMapを作成し、1つのエントリに入れて、その値をオーバーライドし、キーで値を取得しようとします。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }
}

HashMap<AmbiguousInteger, Integer> map = new HashMap<>();
// logically equivalent keys
AmbiguousInteger key1 = new AmbiguousInteger(1),
                 key2 = new AmbiguousInteger(1),
                 key3 = new AmbiguousInteger(1);
map.put(key1, 1); // put in value for entry '1'
map.put(key2, 2); // attempt to override value for entry '1'
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(key3));

Expected: 2, 2, 2

hashCode()およびequals()をオーバーライドしない:デフォルトでJavaは異なるオブジェクトに対して異なるhashCode()値を生成するため、HashMapはこれらを使用しますkey1key2を異なるバケットにマップする値。 key3には対応するバケットがないため、値はありません。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 2, get as entry 2[1]
map.get(key3); // map to no bucket
Expected: 2, 2, 2
Output:   1, 2, null

hashCode()のみをオーバーライド:HashMapkey1key2を同じバケットにマップしますが、デフォルトで[_ = FUNC @]チェックが失敗するため、key1 == key2key1.equals(key2)チェックの両方が失敗するため、異なるエントリのままですequals()==チェックを使用し、異なるインスタンスを参照します。 key3==equals()の両方のチェックに失敗し、key1key2に対するチェックが行われないため、対応する値がありません。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[2]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[2]
map.get(key3); // map to bucket 1, no corresponding entry
Expected: 2, 2, 2
Output:   1, 2, null

equals()のみをオーバーライドします。HashMapは、デフォルトのhashCode()が異なるため、すべてのキーを異なるバケットにマップします。 ==またはequals()チェックは、HashMapがそれらを使用する必要があるポイントに到達しないため、ここでは無関係です。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 2, get as entry 2[1]
map.get(key3); // map to no bucket
Expected: 2, 2, 2
Actual:   1, 2, null

hashCode()equals()の両方をオーバーライドしますHashMapは、key1key2key3を同じバケットにマッピングします。 ==チェックは、異なるインスタンスを比較すると失敗しますが、equals()チェックはすべて同じ値を持ち、ロジックによって「論理的に同等」と見なされるため、パスします。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return value;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   2, 2, 2

hashCode()がランダムな場合はどうなりますか? HashMapは操作ごとに異なるバケットを割り当てるため、以前に入力したものと同じエントリを見つけることはできません。

class AmbiguousInteger {
    private static int staticInt;
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return ++staticInt; // every subsequent call gets different value
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to no bucket, no corresponding value
map.get(key2); // map to no bucket, no corresponding value
map.get(key3); // map to no bucket, no corresponding value
Expected: 2, 2, 2
Actual:   null, null, null

hashCode()が常に同じ場合はどうなりますか? HashMapは、すべてのキーを1つの大きなバケットにマップします。この場合、コードは機能的には正しいですが、HashMapの使用は実質的に冗長です。検索では、その単一バケット内のすべてのエントリをO(N)時間( またはO(logN) for Java 8 )、Listの使用と同等。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   2, 2, 2

equalsが常にfalseの場合はどうなりますか? ==チェックは同じインスタンスをそれ自体と比較すると合格しますが、それ以外の場合は失敗します。equalsチェックは常に失敗するため、key1key2、およびkey3は「論理的に異なる」と見なされ、異なるエントリにマッピングされますが、同じhashCode()のため、まだ同じバケット内にあります。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        return false;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[2]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[2]
map.get(key3); // map to bucket 1, no corresponding entry
Expected: 2, 2, 2
Actual:   1, 2, null

equalsが常に真の場合はどうでしょうか? :基本的に、すべてのオブジェクトは別のオブジェクトと「論理的に同等」であるとみなしているため、すべて同じバケット(同じhashCode()による)にマッピングされます。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value
map.put(new AmbiguousInteger(100), 100); // map to bucket 1, set as entry1[1], override value
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   100, 100, 100
65
hidro

equalsをオーバーライドせずにhashCodeをオーバーライドしました。 equalsが2つのオブジェクトに対してtrueを返すすべての場合、hashCodeが同じ値を返すことを確認する必要があります。ハッシュコードは、2つのオブジェクトが等しい場合に等しくなければならないコードです(逆は真である必要はありません)。ハードコードされた値9を入力すると、再び契約を満たします。

ハッシュマップでは、等式はハッシュバケット内でのみテストされます。あなたの2つの月曜日のオブジェクトは等しいはずですが、それらは異なるハッシュコードを返しているので、equalsメソッドはそれらの等価性を判断するために呼び出されません。も考慮されていません。

44
David M

Effective Java (警告:pdfリンク)の第3章を読む必要があることを十分に強調することはできません。この章では、Objectのメソッドのオーバーライド、特にequalsコントラクトについて知る必要があるすべてを学びます。 Josh Blochには、従うべきequalsメソッドをオーバーライドするための素晴らしいレシピがあります。また、equalsメソッドの特定の実装で==ではなくequalsを使用する必要がある理由を理解するのに役立ちます。

お役に立てれば。読んでください。 (少なくとも最初のいくつかの項目...そして、あなたは残りを読みたいでしょう:-)。

-トム

8
Tom

HashCode()メソッドをオーバーライドしない場合、ToDosクラスはObjectからデフォルトのhashCode()メソッドを継承します。これにより、すべてのオブジェクトに個別のハッシュコードが与えられます。この意味は t1およびt2には2つの異なるハッシュコードがあります。たとえそれらを比較したとしても、それらは等しくなります。特定のハッシュマップの実装に応じて、マップはそれらを個別に自由に保存できます(実際、これは実際に起こります)。

HashCode()メソッドを正しくオーバーライドして、等しいオブジェクトが同じハッシュコードを取得するようにすると、ハッシュマップは2つの等しいオブジェクトを見つけて同じハッシュバケットに配置できます。

より良い実装は、次のようなnot等しいdifferentハッシュコードであるオブジェクトを与えます:

public int hashCode() {
    return (day != null) ? day.hashCode() : 0;
}
6
Avi

コメントすると、3が返されます。

objectから継承されたhashCode()は、3つのToDoオブジェクトに対して3つの異なるハッシュコードを返すだけで呼び出されるためです。ハッシュコードが等しくないということは、3つのオブジェクトが異なるバケットを宛先とし、それぞれのバケットの最初のエントリであるため、equals()がfalseを返すことを意味します。 hashCodeが異なる場合、オブジェクトが等しくないことが事前に理解されています。それらは異なるバケットに入れられます。

コメントを解除すると、2が返されます。

ここでは、オーバーライドされたhashCode()が呼び出され、すべてのToDoに対して同じ値が返され、それらはすべて、直線的に接続された1つのバケットに入る必要があります。等しいハッシュコードは、オブジェクトの平等または不平等について何も約束しません。

t3のhashCode()は9であり、最初のエントリであるため、equals()はfalseであり、t3はバケットに挿入されます(バケット0)。

その後、9と同じhashCode()を取得するt2は同じバケット0に向けられ、バケット0にすでに存在するt3の後続のequals()は、オーバーライドされたequal()の定義によってfalseを返します。

HashCode()が9のt1もbucket0宛てであり、同じバケット内の既存のt2と比較すると、後続のequals()呼び出しはtrueを返します。 t1はマップへの入力に失敗します。したがって、マップの正味サイズは2-> {ToDos @ 9 = cleanAttic、ToDos @ 9 = payBills}です

これにより、equals()とhashCode()の両方を実装することの重要性が説明されます。また、hashCode()を決定する際にequals()の決定に使用されるフィールドも取得する必要があります。これにより、2つのオブジェクトが等しい場合、常に同じhashCodeが保持されます。 hashCodesは、equals()と一貫している必要があるため、擬似乱数として認識されるべきではありません。

4
Shadab Khan

Effective Javaによると、

Equals()をオーバーライドするときは常にhashCode()をオーバーライドします

まあ、なぜですか?単純です。異なるオブジェクト(参照ではなくコンテンツ)が異なるハッシュコードを取得する必要があるためです。一方、等しいオブジェクトは同じハッシュコードを取得する必要があります。

上記によると、Java連想データ構造は、equals()とhashCode()の呼び出しによって得られた結果を比較してバケットを作成します。両方が同じである場合、オブジェクトは等しくなります。

特定のケース(上記の例)では、hashCode()がコメント化の場合、各インスタンス(Objectによって継承された動作)に対してハッシュとして乱数が生成され、equals()はStringの参照をチェックします(Java String Pool)を思い出してください。したがって、equals()はtrueを返す必要がありますが、hashCode()ではなく、結果は3個の異なるオブジェクトが保存されています。hashCode()がコントラクトを尊重しているが、常に9を返す場合はどうなるかを見てみましょうコメント解除 =。まあ、hashCode()は常に同じで、equals()はプール内の2つの文字列(つまり「月曜日」)に対してtrueを返します。そして、それらの場合、バケットは同じで、2つの要素のみが格納されます

したがって、特に複合データ型がユーザー定義であり、Java連想データ構造で使用される場合は、hashCode()およびequals()オーバーライドを使用する際には注意が必要です。

3
Paolo Maresca

ハッシュバケットマッピングの観点からhashCodeを考えるのではなく、もう少し抽象的に考えるほうが便利だと思います。2つのオブジェクトが異なるハッシュコードを持っているという観察は、オブジェクトが等しくないという観察を構成します。その結果、コレクション内のどのオブジェクトも特定のハッシュコードを持たないという観察は、コレクション内のどのオブジェクトもそのハッシュコードを持つオブジェクトと等しくないという観察を構成します。さらに、コレクション内のオブジェクトのいずれかが何らかの特性を持つハッシュコードを持たないという観察は、それらのどれもがどのオブジェクトとも等しくないという観察を構成します。

ハッシュテーブルは通常、特性のファミリーを定義することによって機能し、そのうちの1つが各オブジェクトのハッシュコードに適用可能です(たとえば、「0 mod 47に一致する」、「1 mod 47に一致する」など)。各特性を持つオブジェクトのコレクションを持つ。その後、オブジェクトが与えられ、どの特性がそれに適用されるかを決定できる場合、その特性を持つもののコレクション内にある必要があることを知ることができます。

ハッシュテーブルは通常、一連の番号付きバケットを使用しますが、実装の詳細です。重要なのは、オブジェクトのハッシュコードを使用して、同等ではない可能性が高いため、比較する必要のない多くのものをすばやく識別することです。

0
supercat

HashCodeのコメントが解除されている場合、HashMapはt1とt2を同じものと見なします。したがって、t2の値はt1の値を覆します。これがどのように機能するかを理解するために、hashCodeが2つのインスタンスに対して同じものを返すとき、それらは同じHashMapバケットに行くことに注意してください。同じバケットに2番目のものを挿入しようとすると(この場合、t1が既に存在する場合にt2が挿入されます)、HashMapは、等しい別のキーを求めてバケットをスキャンします。あなたの場合、t1とt2は同じ日であるため等しいです。その時点で、「payBills」の強盗は「doLaundry」です。 t2がt1をキーとして破壊するかどうかについては、これは未定義だと思います。したがって、どちらの動作も許可されます。

ここで考慮すべき重要な点がいくつかあります。

  1. 2つのToDoインスタンスは、曜日が同じという理由だけで本当に等しいのですか?
  2. Equalsを実装するときは常に、hashCodeを実装して、等しい2つのオブジェクトが同じhashCode値を持つようにする必要があります。これは、HashMapが行う基本的な仮定です。これはおそらく、hashCodeメソッドに依存する他のすべての場合にも当てはまります。
  3. ハッシュコードが均等に分散されるようにhashCodeメソッドを設計します。そうしないと、ハッシュのパフォーマンス上の利点が得られません。この観点から、9を返すことはあなたができる最悪のことの1つです。
0
allyourcode

Javaで新しいオブジェクトを作成するたびに、JVM自体によって一意のハッシュコードが割り当てられます。 hashcodeメソッドをオーバーライドしない場合、オブジェクトは一意のhascodeを取得し、一意のバケットを取得します(Imagineバケットは、JVMがオブジェクトを検索するメモリ内の場所にすぎません)。

(ハッシュコードの一意性を確認するには、各オブジェクトでhashcodeメソッドを呼び出し、コンソールに値を出力します)

ハッシュコードメソッドのコメントを解除している場合、ハッシュマップはまず、メソッドが返すハッシュコードと同じハッシュコードを持つバケットを探します。そして、同じハッシュコードを返すたびに。ハッシュマップがそのバケットを見つけると、euqalsメソッドを使用して、現在のオブジェクトとバケットにあるオブジェクトを比較します。ここでは、「Monday」が検出されるため、同じハッシュコードと同じ等式実装を持つオブジェクトが既に存在するため、ハッシュマップの実装では再度追加できません。

ハッシュコードメソッドをコメントすると、JVMは3つのオブジェクトすべてに対して異なるハッシュコードを返すだけであるため、equalsメソッドを使用してオブジェクトを結合することさえ気にしません。したがって、ハッシュマップの実装によって追加されるMapには3つの異なるオブジェクトがあります。

0
Hiren Savalia