私の理解によれば、私は思います:
私は正しいですか?
正しい場合は、次の質問があります。HashMap
は内部的にオブジェクトのハッシュコードを使用しています。それで、2つのオブジェクトが同じハッシュコードを持つことができるならば、HashMap
はそれがどのキーを使うかをどうやって追跡できますか?
誰かがHashMap
がオブジェクトのハッシュコードを内部的に使用する方法を説明できますか?
ハッシュマップは次のように機能します(これは少し簡略化されていますが、基本的なメカニズムを示しています)。
キーと値のペアを格納するために使用する「バケット」がいくつかあります。各バケットには一意の番号があります。これがバケットを識別します。キーと値のペアをマップに入れると、ハッシュマップはそのキーのハッシュコードを調べて、識別子がそのキーのハッシュコードであるバケットにそのペアを格納します。たとえば、キーのハッシュコードは235 - >ペアはバケット番号235に格納されます(1つのバケットに複数のキーと値のペアを格納できることに注意してください)。
ハッシュマップの中で値を検索するとき、それにキーを与えることで、あなたが与えたキーのハッシュコードを最初に調べます。ハッシュマップは対応するバケットを調べ、equals()
と比較して、指定したキーとバケット内のすべてのペアのキーを比較します。
これで、マップ内のキーと値のペアを調べるのに非常に効率的であることがわかります。キーのハッシュコードによって、ハッシュマップはすぐにどのバケットを見るべきかを知っているので、そのバケットの内容に対してテストするだけで済みます。
上記のメカニズムを見ると、キーのhashCode()
メソッドとequals()
メソッドに必要な要件もわかります。
2つのキーが同じ場合(それらを比較するとequals()
はtrue
を返します)、それらのhashCode()
メソッドは同じ番号を返さなければなりません。キーがこれに違反している場合、等しいキーが異なるバケットに格納される可能性があり、ハッシュマップはキーと値のペアを見つけることができません(同じバケットを検索するため)。
2つのキーが異なる場合は、それらのハッシュコードが同じかどうかは関係ありません。ハッシュコードが同じ場合、それらは同じバケットに格納されます。この場合、ハッシュマップはそれらを区別するためにequals()
を使用します。
あなたの3番目の主張は間違っています。
2つの異なるオブジェクトが同じハッシュコードを持つことは完全に合法です。指定されたキーを持つ可能なエントリをすばやく見つけることができるように、HashMap
によって「初回通過フィルタ」として使用されます。同じハッシュコードを持つキーは、指定されたキーと等しいかどうかがテストされます。
2つの異なるオブジェクトが同じハッシュコードを持つことができないという要件は望ましくありません。32 可能なオブジェクト(また、他のクラスが同じハッシュを生成する可能性があるため、異なる型がオブジェクトのフィールドを使用してハッシュコードを生成することすらできないことも意味します。)
HashMap
はEntry
オブジェクトの配列です。
HashMap
を単なるオブジェクトの配列と見なします。
このObject
が何であるかを見てください。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
…
}
各Entry
オブジェクトは、キーと値のペアを表します。バケットに複数のnext
がある場合、フィールドEntry
は別のEntry
オブジェクトを参照します。
2つの異なるオブジェクトのハッシュコードが同じであることが時々起こるかもしれません。この場合、2つのオブジェクトが1つのバケットに保存され、リンクリストとして表示されます。エントリポイントは、最近追加されたオブジェクトです。このオブジェクトは、next
フィールドを持つ別のオブジェクトを参照します。最後のエントリはnull
を参照しています。
デフォルトのコンストラクタでHashMap
を作成した場合
HashMap hashMap = new HashMap();
アレイはサイズ16、デフォルトは0.75のロードバランスで作成されます。
hash % (arrayLength-1)
を計算します(バケット番号)HashMap
にすでに保存されているキーを使用して値を追加しようとすると、値は上書きされます。バケットにすでに少なくとも1つの要素がある場合は、新しい要素が追加されてバケットの最初の位置に配置されます。そのnext
フィールドは、古い要素を参照しています。
hash % (arrayLength-1)
Entry
が見つかるでしょう。目的の要素が見つからない場合はnull
を返しますあなたは http://javarevisited.blogspot.com/2011/02/how-hashmap-works-in-Java.html で優れた情報を見つけることができます
要約する:
HashMapはハッシュの原理に基づいて動作します
put(key、value):HashMapはキーと値の両方のオブジェクトをMap.Entryとして保存します。ハッシュマップはバケットを取得するためにハッシュコード(key)を適用します。衝突があると、HashMapはLinkedListを使用してオブジェクトを格納します。
get(key):HashMapはKey Objectのハッシュコードを使用してバケットの場所を見つけ、次にkeys.equals()メソッドを呼び出してLinkedList内の正しいノードを識別して戻ります。 Java HashMap内のそのキーに関連付けられた値オブジェクト。
Java 8
バージョンの場合のHashMap
のメカニズムの大まかな説明は、次のとおりです(Java 6とは若干異なる場合があります)。
hash()
によって計算され、ハッシュテーブルのどのバケットを与えられたキーに使うかを決定します。Map.Entry
HashMap.Node
ノードのリンクリストバージョン。
それは表すことができます:
HashMap.TreeNode
Node[] table
Set<Map.Entry> entrySet
エンティティのセット。int size
float loadFactor
int threshold
threshold = capacity * loadFactor
int hash(key)
ハッシュをバケットにマッピングする方法
以下の論理を使用してください。
static int hashToBucket(int tableSize, int hash) { return (tableSize - 1) & hash; }
ハッシュテーブルでは、容量はバケット数を意味し、それはtable.length
から取得できます。threshold
およびloadFactor
を介して計算することもできます。したがって、クラスフィールドとして定義する必要はありません。
有効容量は、次の方法で取得できます。capacity()
threshold
に達すると、ハッシュテーブルの容量(table.length
)を2倍にしてから、すべての要素に対して再ハッシュを実行してテーブルを再構築します。O(1)
です。O(1)
を介してアクセスされます。O(1)
として表示される可能性があります。O(1)
ではなくO(log N)
と見なすことができるためです。ハッシュコードは、ハッシュマップのどのバケットをチェックするかを決定します。バケット内に複数のオブジェクトがある場合は、バケット内のどの項目が(equals()
メソッドを使用して)目的の項目に一致するかを検索するために線形検索が行われます。
言い換えれば、もしあなたが完璧なハッシュコードを持っていてもハッシュマップアクセスは一定であれば、あなたはバケツを通して繰り返す必要は決してないでしょう。スペース所要量を削減します。あなたが最悪のハッシュコードを持っている(常に同じ数を返す)なら、あなたが欲しいものを得るためにあなたがマップ内のすべてのアイテムを通して検索しなければならないので(それらはすべて同じバケットにある).
ほとんどの場合、よく書かれたハッシュコードは完璧ではありませんが、ほぼ一定のアクセスを提供するのに十分なほどユニークです。
あなたはポイント3を間違えています。 2つのエントリは同じハッシュコードを持つことができますが、等しくなることはできません。 OpenJdkのHashMap.get の実装を見てください。ハッシュが等しいこととキーが等しいことを確認していることがわかります。ポイント3が真であれば、キーが等しいことを確認する必要はありません。前者がより効率的な比較であるため、ハッシュコードはキーの前に比較されます。
これについてもう少し詳しく知りたい場合は、ウィキペディアの Open Addressing collision resolution の記事を見てください。これは、OpenJdkの実装で使用されているメカニズムだと思います。そのメカニズムは、他の答えの1つが述べている「バケツ」アプローチとは微妙に異なります。
import Java.util.HashMap;
public class Students {
String name;
int age;
Students(String name, int age ){
this.name = name;
this.age=age;
}
@Override
public int hashCode() {
System.out.println("__hash__");
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
System.out.println("__eq__");
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Students other = (Students) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public static void main(String[] args) {
Students S1 = new Students("taj",22);
Students S2 = new Students("taj",21);
System.out.println(S1.hashCode());
System.out.println(S2.hashCode());
HashMap<Students,String > HM = new HashMap<Students,String > ();
HM.put(S1, "tajinder");
HM.put(S2, "tajinder");
System.out.println(HM.size());
}
}
Output:
__ hash __
116232
__ hash __
116201
__ hash __
__ hash __
2
そのため、ここでは、オブジェクトS1とS2の両方のコンテンツが異なる場合、オーバーライドされたHashcodeメソッドが両方のオブジェクトに対して異なるHashcode(116232、11601)を生成することを確信しています。今は違うハッシュコードがあるので、EQUALSメソッドを呼ぶのも面倒ではありません。異なるハッシュコードは、オブジェクト内の異なるコンテンツを保証するからです。
public static void main(String[] args) {
Students S1 = new Students("taj",21);
Students S2 = new Students("taj",21);
System.out.println(S1.hashCode());
System.out.println(S2.hashCode());
HashMap<Students,String > HM = new HashMap<Students,String > ();
HM.put(S1, "tajinder");
HM.put(S2, "tajinder");
System.out.println(HM.size());
}
}
Now lets change out main method a little bit. Output after this change is
__ hash __
116201
__ hash __
116201
__ hash __
__ hash __
__ eq __
1
We can clearly see that equal method is called. Here is print statement __eq__, since we have same hashcode, then content of objects MAY or MAY not be similar. So program internally calls Equal method to verify this.
Conclusion
If hashcode is different , equal method will not get called.
if hashcode is same, equal method will get called.
Thanks , hope it helps.
各Entryオブジェクトはキーと値のペアを表します。バケットに複数のエントリがある場合、フィールドnextは他のエントリオブジェクトを参照します。
2つの異なるオブジェクトのハッシュコードが同じであることが時々起こるかもしれません。この場合、2つのオブジェクトが1つのバケットに保存され、LinkedListとして表示されます。エントリポイントは、最近追加されたオブジェクトです。このオブジェクトは、次のフィールドを持つ他のオブジェクトを参照します。最後のエントリがnullを参照しています。デフォルトのコンストラクタでHashMapを作成するとき
配列はサイズ16、デフォルトは0.75のロードバランスで作成されます。
ハッシュマップはハッシュの原理に基づいて動作します
HashMapのget(Key k)メソッドは、キーオブジェクトのhashCodeメソッドを呼び出し、返されたhashValueを独自の静的ハッシュ関数に適用して、キーと値がEntry(Map)と呼ばれるネストクラスの形式で格納されるバケット位置(バッキング配列)を探します。エントリ)。したがって、前の行から、キーと値の両方がEntryオブジェクトの形式としてバケットに格納されていると結論付けました。そのため、値だけがバケットに格納されていると考えることは正しくなく、インタビュアーに良い印象を与えることはありません。
Keyがnullの場合、nullキーは常にハッシュ0、つまりインデックス0にマッピングされます。
Keyがnullでない場合、keyオブジェクトのhashfunctionを呼び出します。上記のメソッドの4行目、つまりkey.hashCode()を参照してください。したがって、key.hashCode()がhashValueを返した後、4行目は次のようになります。
int hash = hash(hashValue)
そして今、返されたhashValueをそれ自身のハッシュ関数に適用します。
Hash(hashValue)を使ってハッシュ値をもう一度計算しているのはなぜだろうか。答えは質の悪いハッシュ関数から守ることです。
これで、Entryオブジェクトが格納されているバケットの場所を見つけるために最終ハッシュ値が使用されます。エントリオブジェクトは、このようにバケットに格納します(ハッシュ、キー、値、バケットインデックス)。
HashMapのしくみの詳細については説明しませんが、HashMapのしくみを現実と関連付けることで、HashMapのしくみを思い出すことができるように例を示します。
Key、Value、HashCode、そしてbucketがあります。
しばらくの間、それぞれを以下のように関連付けます。
Map.get(key)を使う:
StevieはVIP社会の別荘に住んでいる彼の友人の(Josse)家に行きたがっています。それをJavaLovers Societyにしましょう。 Josseの住所は彼のSSNです(これは誰にとっても異なります)。 SSNに基づいて協会の名前を見つけるための索引が維持されています。このインデックスはHashCodeを見つけるためのアルゴリズムと考えることができます。
Map.put(key、Value)を使う
これはHashCodeを見つけることによってこの値に適した社会を見つけ、そして値が格納されます。
私はこれが助けになることを願っていますし、これは変更の余地があります。
2つのオブジェクトが等しいということは、それらが同じハッシュコードを持っていることを意味しますが、その逆はありません。
HashMapのJava 8アップデート -
あなたはあなたのコードの中でこの操作をします -
myHashmap.put("old","key-value-pair");
myHashMap.put("very-old","old-key-value-pair");
そのため、両方のキー"old"
と"very-old"
に対して返されたハッシュコードが同じであるとします。それではどうなるでしょう。
myHashMap
はHashMapです。最初は容量を指定しなかったとします。 Javaあたりのデフォルト容量は16です。したがって、newキーワードを使用してハッシュマップを初期化するとすぐに、16個のバケットが作成されます。最初のステートメントを実行したときに
myHashmap.put("old","key-value-pair");
それから"old"
のためのハッシュコードが計算されます、そして、ハッシュコードも非常に大きい整数であるかもしれないので、それで、Javaは内部的にこれをしました -
hash XOR hash >>> 16
これを大きくすると、0から15の間のインデックスが返されます。これで、キー値のペア"old"
と"key-value-pair"
は、Entryオブジェクトのキーと値のインスタンス変数に変換されます。そして、このエントリオブジェクトはバケットに格納されます。または、特定のインデックスにこのエントリオブジェクトが格納されると言えます。
FYIエントリは、これらのシグネチャ/定義を持つMapインタフェースMap.Entryのクラスです。
class Entry{
final Key k;
value v;
final int hash;
Entry next;
}
今度は次の文を実行するとき -
myHashmap.put("very-old","old-key-value-pair");
"very-old"
は"old"
と同じハッシュコードを与えるので、この新しいキーと値のペアは同じインデックスまたは同じバケットに送信されます。しかし、このバケットは空ではないため、Entryオブジェクトのnext
変数を使用してこの新しいキーと値のペアを格納します。
これは、同じハッシュコードを持つすべてのオブジェクトのリンクリストとして格納されますが、TRIEFY_THRESHOLDは値6で指定されます。ルート。
言われているように、絵は1000語の価値があります。私は言う:いくつかのコードは1000語以上です。これがHashMapのソースコードです。メソッドを取得:
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
そのため、ハッシュが「バケット」を見つけるために使用され、最初の要素が常にそのバケットでチェックされることが明らかになります。そうでない場合は、キーのequals
を使用してリンクリスト内の実際の要素を見つけます。
put()
メソッドを見てみましょう:
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
もう少し複雑ですが、新しい要素がハッシュに基づいて計算された位置のタブに配置されることが明らかになります。
ここでi = (n - 1) & hash
ここでi
は新しい要素が置かれるインデックスです(またはそれは "バケツ"です)。 n
はtab
配列( "バケット"の配列)のサイズです。
まず、その「バケツ」の最初の要素として入れることを試みます。すでに要素がある場合は、新しいノードをリストに追加します。