56

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

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 を返します。なんで?

4

9 に答える 9

99

HashMaphashCode()==および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()and をオーバーライドしないでくださいequals()。デフォルトでは、Java はhashCode()オブジェクトごとに異なる値を生成するため、HashMapこれらの値を使用して異なるバケットにマッピングkey1します。対応するバケットがないため、値がありません。key2key3

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()のみ: とを同じバケットにHashMapマップしますが、デフォルトではチェックを使用し、異なるインスタンスを参照するため、とチェックの両方が失敗するため、それらは異なるエントリのままです。は両方とも失敗し、andに対してチェックするため、対応する値はありません。key1key2key1 == 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(): HashMapmapskey1の両方key2オーバーライドkey3し、同じバケットに入れます。==異なるインスタンスを比較するとチェックは失敗します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) 時間 ( Java 8 の場合は O(logN) )でその単一のバケット内のすべてのエントリを反復処理する必要があるためです。の使用に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チェックは常に失敗しkey1、 「論理的に異なる」key2key3見なされ、異なるエントリにマップされますが、同じために同じバケット内にあります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が常に true の場合はどうなりますか? : 基本的に、すべてのオブジェクトが別のオブジェクトと「論理的に同等」であると見なされると言っているため、それらはすべて同じバケット (同じため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
于 2016-07-25T04:22:54.510 に答える
49

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

ハッシュ マップでは、等価性はハッシュ バケット内でのみテストされます。2 つの Monday オブジェクトは等しいはずですが、それらが異なるハッシュ コードを返すため、等しいequalsかどうかを判断するためにメソッドが呼び出されることさえありません。それらは直接異なるバケットに入れられ、等しい可能性も考慮されません。

于 2009-12-12T19:04:36.397 に答える
8

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

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

-トム

于 2009-12-12T20:08:06.670 に答える
7

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

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

より良い実装では、次のように、異なるハッシュコードではないオブジェクトが返されます。

public int hashCode() {
    return (day != null) ? day.hashCode() : 0;
}
于 2009-12-12T19:06:30.747 に答える
4

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

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

コメントを外すと、2が返されます。

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

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

次に、9と同じhashCode()を取得するt2は同じbucket0に送信され、bucket0にすでに存在するt3の後続のequals()は、オーバーライドされたequal()の定義によってfalseを返します。

これで、hashCode()が9のt1もbucket0に送信され、同じバケット内の既存のt2と比較すると、後続のequals()呼び出しはtrueを返します。t1はマップに入ることができません。したがって、マップの正味サイズは2-> {ToDos @ 9 = cleanAttic、ToDos @ 9 = payBills}

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

于 2011-09-24T06:57:25.893 に答える
0

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 つです。
于 2009-12-12T19:36:17.593 に答える