HashMapはリンクリストの先頭または末尾に挿入されていますか?
jdk1.8の前にヘッドに挿入され、jdk1.8でテールに挿入されました。
リンクリストの挿入位置の分析、キーはHashMapのputメソッドを分析することです。
jdk1.6
putメソッドのコードは次のとおりです。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果发现key已经在链表中存在,则修改并返回旧的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果遍历链表没发现这个key,则会调用以下代码
modCount++;
addEntry(hash, key, value, i);
return null;
}
リンクリストから対応するキー値が見つからない場合、addEntryメソッドが呼び出され、リンクリストにエントリが追加されます。フォーカスは、addEntryメソッドでリンクリストを挿入する方法にあります。addEntryメソッドのソースコードは次のとおりです。
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
ここで新しいエントリオブジェクトが構築され(構築メソッドの最後のパラメータが現在のエントリリストに渡されます)、古いエントリリストがこの新しいエントリオブジェクトに直接置き換えられます。これはヘッド挿入メソッドであると推測できます。もう一度見てくださいエントリーの作成方法:
Entry( int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
nexrt = nの作成方法から、元のリンクリストが実際に新しく作成されたEntryオブジェクトの背後に直接リンクされていることがわかり、ヘッドに挿入されていると結論付けることができます。
jdk1.8 putメソッドのコードは次のとおりです。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
jdk1.8では、リンクリストの長さが8より大きい場合、赤黒木に変換されるため、ソースコードはjdk1.6よりもはるかに複雑に見え、if-elseの判断の多くは赤黒木とリンクリストの状況に対処することです。コアコードの次の行のみが挿入されます。
for (int binCount = 0; ; ++binCount) {
//e是p的下一个节点
if ((e = p.next) == null) {
//插入链表的尾部
p.next = newNode(hash, key, value, null);
//如果插入后链表长度大于8则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果key在链表中已经存在,则退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
//如果key在链表中已经存在,则修改其原先的key值,并且返回老的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
リンクリストが挿入されるコードは次のとおりです。
//e是p的下一个节点
if ((e = p.next) == null) {
//插入链表的尾部
p.next = newNode(hash, key, value, null);
//如果插入后链表长度大于8则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
このコードから、リンクリストの最後に到達すると(つまり、pはリンクリストの最後のノード)、eがnullに割り当てられ、このブランチコードに入り、newNodeメソッドを使用して新しいノードの挿入が確立されることがはっきりとわかります。テール。
結論:リンクリストの末尾がjdk1.8に挿入されます
jdk1.8にはエントリがありません。jdk1.6には、Map.Entryインターフェースを実装するHashMapに組み込みのEntryクラスがありますが、jdk1.8ではEntryクラスがなくなり、Nodeクラスになりました。 Map.Entryインターフェースを実装しました。これは、jdk1.6のエントリーに相当します。
HashMapを展開する際の無限ループの問題
HashMapは、配列+リンクリストを使用します。配列は固定長です。リンクされたリストが長すぎる場合、リンクされたリストの長さを減らすために、配列の長さをリハッシュするために拡張する必要があります。2つのスレッドが同時に拡張をトリガーした場合、ノードを移動すると、リンクリスト内の2つのノードが相互に参照し、リングリンクリストが生成されます。
HashMapの容量が2の累乗である理由
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //将空key的Entry加入到table[0]中
int hash = hash(key.hashCode()); //计算key.hashcode()的hash值,hash函数由hashmap自己实现
int i = indexFor(hash, table.length);//获取将要存放的数组下标
/*
* for中的代码用于:当hash值相同且key相同的情况下,使用新值覆盖旧值(其实就是修改功能)
*/
for (Entry<K, V> e = table[i]; e != null; e = e.next) {//注意:for循环在第一次执行时就会先判断条件
Object k;
//hash值相同且key相同的情况下,使用新值覆盖旧值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//e.recordAccess(this);
return oldValue;//返回旧值
}
}
modCount++;
addEntry(hash, key, value, i);//增加一个新的Entry到table[i]
return null;//如果没有与传入的key相等的Entry,就返回null
}
/**
* "按位与"来获取数组下标
*/
static int indexFor(int h, int length) {
return h & (length - 1);
}
HashMapは常にバケットを2のn乗に保ちます。なぜこれが原因ですか?indexForメソッドはこの問題を説明しています。コンピュータのビット操作は基本的な操作であることは誰もが知っています。ビット操作の効率は、残りの%を取る操作よりもはるかに高くなります。
例:2 ^ nをバイナリに変換すると、1 + n 0sになります。1を引いた後、1は0 + n 1sになります。例:16-> 10000、15-> 01111次に、&ビット演算のルールに従って、すべて1(true )、それは1、それから0≤演算後の結果≤15(h <= 15と仮定)、演算後の結果はh自体、h> 15、演算後の結果は&演算の後の最後の4ビット結局のところ、値は%計算後の余りです。
容量が2 ^ nでなければならない場合、h&(length-1)== h%length