私はこのテストコードを持っています:
_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を返します。どうして?
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
はこれらを使用しますkey1
とkey2
を異なるバケットにマップする値。 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()
のみをオーバーライド:HashMap
はkey1
とkey2
を同じバケットにマップしますが、デフォルトで[_ = FUNC @]チェックが失敗するため、key1 == key2
とkey1.equals(key2)
チェックの両方が失敗するため、異なるエントリのままですequals()
は==
チェックを使用し、異なるインスタンスを参照します。 key3
は==
とequals()
の両方のチェックに失敗し、key1
とkey2
に対するチェックが行われないため、対応する値がありません。
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
は、key1
、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)時間( または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
チェックは常に失敗するため、key1
、key2
、および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
equals
をオーバーライドせずにhashCode
をオーバーライドしました。 equals
が2つのオブジェクトに対してtrueを返すすべての場合、hashCode
が同じ値を返すことを確認する必要があります。ハッシュコードは、2つのオブジェクトが等しい場合に等しくなければならないコードです(逆は真である必要はありません)。ハードコードされた値9を入力すると、再び契約を満たします。
ハッシュマップでは、等式はハッシュバケット内でのみテストされます。あなたの2つの月曜日のオブジェクトは等しいはずですが、それらは異なるハッシュコードを返しているので、equals
メソッドはそれらの等価性を判断するために呼び出されません。も考慮されていません。
Effective Java (警告:pdfリンク)の第3章を読む必要があることを十分に強調することはできません。この章では、Object
のメソッドのオーバーライド、特にequals
コントラクトについて知る必要があるすべてを学びます。 Josh Blochには、従うべきequals
メソッドをオーバーライドするための素晴らしいレシピがあります。また、equals
メソッドの特定の実装で==
ではなくequals
を使用する必要がある理由を理解するのに役立ちます。
お役に立てれば。読んでください。 (少なくとも最初のいくつかの項目...そして、あなたは残りを読みたいでしょう:-)。
-トム
HashCode()メソッドをオーバーライドしない場合、ToDosクラスはObjectからデフォルトのhashCode()メソッドを継承します。これにより、すべてのオブジェクトに個別のハッシュコードが与えられます。この意味は t1
およびt2
には2つの異なるハッシュコードがあります。たとえそれらを比較したとしても、それらは等しくなります。特定のハッシュマップの実装に応じて、マップはそれらを個別に自由に保存できます(実際、これは実際に起こります)。
HashCode()メソッドを正しくオーバーライドして、等しいオブジェクトが同じハッシュコードを取得するようにすると、ハッシュマップは2つの等しいオブジェクトを見つけて同じハッシュバケットに配置できます。
より良い実装は、次のようなnot等しいdifferentハッシュコードであるオブジェクトを与えます:
public int hashCode() {
return (day != null) ? day.hashCode() : 0;
}
コメントすると、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()と一貫している必要があるため、擬似乱数として認識されるべきではありません。
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()オーバーライドを使用する際には注意が必要です。
ハッシュバケットマッピングの観点からhashCode
を考えるのではなく、もう少し抽象的に考えるほうが便利だと思います。2つのオブジェクトが異なるハッシュコードを持っているという観察は、オブジェクトが等しくないという観察を構成します。その結果、コレクション内のどのオブジェクトも特定のハッシュコードを持たないという観察は、コレクション内のどのオブジェクトもそのハッシュコードを持つオブジェクトと等しくないという観察を構成します。さらに、コレクション内のオブジェクトのいずれかが何らかの特性を持つハッシュコードを持たないという観察は、それらのどれもがどのオブジェクトとも等しくないという観察を構成します。
ハッシュテーブルは通常、特性のファミリーを定義することによって機能し、そのうちの1つが各オブジェクトのハッシュコードに適用可能です(たとえば、「0 mod 47に一致する」、「1 mod 47に一致する」など)。各特性を持つオブジェクトのコレクションを持つ。その後、オブジェクトが与えられ、どの特性がそれに適用されるかを決定できる場合、その特性を持つもののコレクション内にある必要があることを知ることができます。
ハッシュテーブルは通常、一連の番号付きバケットを使用しますが、実装の詳細です。重要なのは、オブジェクトのハッシュコードを使用して、同等ではない可能性が高いため、比較する必要のない多くのものをすばやく識別することです。
HashCodeのコメントが解除されている場合、HashMapはt1とt2を同じものと見なします。したがって、t2の値はt1の値を覆します。これがどのように機能するかを理解するために、hashCodeが2つのインスタンスに対して同じものを返すとき、それらは同じHashMapバケットに行くことに注意してください。同じバケットに2番目のものを挿入しようとすると(この場合、t1が既に存在する場合にt2が挿入されます)、HashMapは、等しい別のキーを求めてバケットをスキャンします。あなたの場合、t1とt2は同じ日であるため等しいです。その時点で、「payBills」の強盗は「doLaundry」です。 t2がt1をキーとして破壊するかどうかについては、これは未定義だと思います。したがって、どちらの動作も許可されます。
ここで考慮すべき重要な点がいくつかあります。
Javaで新しいオブジェクトを作成するたびに、JVM自体によって一意のハッシュコードが割り当てられます。 hashcodeメソッドをオーバーライドしない場合、オブジェクトは一意のhascodeを取得し、一意のバケットを取得します(Imagineバケットは、JVMがオブジェクトを検索するメモリ内の場所にすぎません)。
(ハッシュコードの一意性を確認するには、各オブジェクトでhashcodeメソッドを呼び出し、コンソールに値を出力します)
ハッシュコードメソッドのコメントを解除している場合、ハッシュマップはまず、メソッドが返すハッシュコードと同じハッシュコードを持つバケットを探します。そして、同じハッシュコードを返すたびに。ハッシュマップがそのバケットを見つけると、euqalsメソッドを使用して、現在のオブジェクトとバケットにあるオブジェクトを比較します。ここでは、「Monday」が検出されるため、同じハッシュコードと同じ等式実装を持つオブジェクトが既に存在するため、ハッシュマップの実装では再度追加できません。
ハッシュコードメソッドをコメントすると、JVMは3つのオブジェクトすべてに対して異なるハッシュコードを返すだけであるため、equalsメソッドを使用してオブジェクトを結合することさえ気にしません。したがって、ハッシュマップの実装によって追加されるMapには3つの異なるオブジェクトがあります。