記事ディレクトリ
JDK1.7での拡張メカニズム
JDK1.7でのresize()メソッドは次のようになります。
voidsize(int newCapacity) { Entry[] oldTable = テーブル; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { しきい値 = Integer.MAX_VALUE; 戻る; }
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
コードからわかるように、元のテーブルの長さが上限に達すると、拡張されなくなります。
上限に達していない場合は、新しいテーブルを作成し、転送メソッドを呼び出します。
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next; //注释1
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //注释2
e.next = newTable[i]; //注释3
newTable[i] = e; //注释4
e = next; //注释5
}
}
}
転送メソッドの機能は、ヘッド挿入メソッドを使用して、元のテーブルのノードを新しいテーブルに配置することです。つまり、新しいテーブルのリンク リストの順序が古いリストの順序と逆になります。 HashMap スレッドのセキュリティが確保されていない場合、このヘッド挿入方法によりリング ノードが発生する可能性があります。
while ループはヘッダー挿入のプロセスを記述します。このロジックは少し複雑です。このコードを分析する例を見てみましょう。
table[1]=3 など、元のテーブルに記録されているリンク リストが 3–>5–>7 であると仮定すると、処理フローは次のようになります。
1. 注 1: e.next の値を記録します。先頭の e は table[1] なので、e3、次へ5、この時点では next==5 となります。
2. 注 2、newTable の e のノードを計算します。ヘッド補間の逆順の結果を示すために、e が newTable[1] のリンク リストに再度ハッシュされると仮定します。
3. 注 3、newTable [1] を e.next に割り当てます。newTable は新しく作成されるので、 newTable[1]null なので、この時点では 3.nextヌル。
4. 注 4、e は newTable[1] に割り当てられます。この時点では newTable[1]=3 です。
5. 注 5、next は e に割り当てられます。このとき、e==5となります。
このとき、最初のノードnode 3がnewTable[1]に追加され、以下で第2サイクルに入り、第2サイクルの開始時にe==5となる。
1. 注 1: e.next の値を記録します。5.次は 7 なので、次 == 7 となります。
2. 注 2、newTable の e のノードを計算します。ヘッド補間の逆順の結果を示すために、e が newTable[1] のリンク リストに再度ハッシュされると仮定します。
3. 注 3、newTable [1] を e.next に割り当てます。newTable[1] は 3 (前のループの注 4 を参照)、e は 5 であるため、5.next==3 となります。
4. 注 4、e は newTable[1] に割り当てられます。この時点では newTable[1]==5 です。
5. 注 5、next は e に割り当てられます。このとき、e==7となります。
このとき、newTable[1] は 5 で、リンク リストの順序は 5->3 です。
以下の 3 番目のループに入り、2 番目のループの先頭に e==7 を入力します。
1. 注 1: e.next の値を記録します。7.next は NULL なので、next==NULL になります。
2. 注 2、newTable の e のノードを計算します。ヘッド補間の逆順の結果を示すために、e が newTable[1] のリンク リストに再度ハッシュされると仮定します。
3. 注 3、newTable [1] を e.next に割り当てます。newTable[1] は 5 (前のループの注 4 を参照)、e は 7 であるため、7.next==5 となります。
4. 注 4、e は newTable[1] に割り当てられます。この時点では newTable[1]==7 です。
5. 注 5、next は e に割り当てられます。このとき e==NULL です。
このとき、newTable[1]は7となりループは終了し、リンクリストの順序は7→5→3となり、元のリンクリストの順序とは逆になります。
注: この逆順展開メソッドは、マルチスレッドの場合、循環リンク リストになる可能性があります。循環リンク リストの理由はおそらく次のとおりです: スレッド 1 はノードを処理する準備ができており、スレッド 2 は HashMap を正常に展開し、リンク リストは逆にソートされ、スレッド 1 がノードの処理中に循環リンク リストが表示されることがあります。
さらに、indexFor(e.hash, newCapacity) について説明します。このメソッドは、新しいテーブル内のノードの添え字を計算するために使用されます。このメソッドのコードは次のとおりです。
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
添え字を計算するアルゴリズムは非常に単純です。ハッシュ値と (length-1) はビット単位で AND 演算されます。length-1 を使用する重要な点は、length が 2 の倍数であるため、length-1 の各ビットは 2 進数で 1 になることです。これにより、ハッシュ値のハッシュが最大限に保証されます。そうでない場合、1 つのビットが 0 の場合、ハッシュ値の対応するビットが 1 か 0 かに関係なく、ビットごとの AND の結果は 0 となり、ハッシュの重複が発生します。結果。
JDK1.8での拡張メカニズム
JDK1.8 では、resize() メソッドが大幅に調整されています。JDK1.8 のsize() メソッドは次のとおりです。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //注释1
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({
"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//注释2
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) //注释3
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//注释4
if (loTail == null) //注释5
loHead = e;
else
loTail.next = e; //注释6
loTail = e; //注释7
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
/注释8
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
コード分析:
-
Simply() メソッドでは、元のテーブルの長さを記録するために oldCap パラメータが定義され、新しいテーブルの長さを記録するために newCap パラメータが定義されています。newCap は oldCap (注 1) の 2 倍の長さであり、拡張子はポイントも2倍になります。
-
注 2 は、元のテーブルを循環し、元のテーブルの各リンク リストの各要素を新しいテーブルに配置することです。
-
注 3、e.next==null は、リンクされたリストに要素が 1 つだけあることを意味するため、e を新しいテーブルに直接入力します。ここで、e.hash & (newCap - 1) はテーブル内の e の位置を計算します。新しいテーブルと JDK1.7 のindexFor() メソッドは別のものです。
-
コメント // 順序を保持します。このコメントはソース コードに付属しており、ここで 4 つの変数が定義されています: loHead、loTail、hiHead、hiTail。少しめまいがするように思えるかもしれませんが、実際、これは JDK1.8 のコンピューティング ノードの添字付けを反映しています。テーブル 新しいアイデア:
通常の状況では、テーブル内のノードの添字の計算方法は hash&(oldTable.length-1) です。拡張後、テーブルの長さは 2 倍になり、テーブルの添字の計算方法は hash& になります。 (newTable.length-1)、つまり hash& (oldTable.length*2-1) であるため、次の結論に達します。新しい添え字と古い添え字の結果は 2 回計算されるか、同じであるか、新しいsubscript は、古い添字に古い配列の長さを加えたものに等しくなります。
たとえば、テーブルの元の長さが 16、拡張後の長さが 32 であるとすると、拡張前後のハッシュ値のテーブル添字は次のように計算されます。
ハッシュ値の各バイナリ ビットは abcde で表され、ハッシュと古いテーブルと新しいテーブルのビットごとの AND の結果、最後の 4 ビットは明らかに同じで、唯一の違いは 5 番目のビットです。 b が位置するビットが 0 の場合、新しいテーブルのビットごとの AND の結果は古いテーブルの結果と同じになります。そうでない場合、b が位置するビットが 0 の場合は、古いテーブルの結果と同じになります。新しいテーブルのビットごとの AND の結果である 1 は、古いテーブルの結果より 10000 (バイナリ) 大きくなっており、このバイナリ 10000 は古いテーブルの長さ 16 です。
つまり、ハッシュ値の新しいハッシュ添字に古いテーブルの長さを足す必要があるのでしょうか? ハッシュ値の5番目のビットが1であるかどうかを確認するだけで十分です。ビット演算の方法はハッシュ値です。および 10000 (つまり、古いテーブルの長さ) ビットごとの AND の場合、結果は 10000 または 00000 のみになります。
したがって、注 4 の e.hash & oldCap は、位置 b が 0 か 1 かを計算するために使用されます。結果が 0 である限り、新しいハッシュの添字は元のハッシュの添字と等しくなります。それ以外の場合、新しいハッシュの座標は次のようになります。元のハッシュ座標に元のテーブルの長さを加えたものに基づきます。
上記の原則を理解すると、このコードは理解しやすくなります。コード内で定義されている 4 つの変数は次のとおりです。
loHead、添字が変更されていないリンクされたリストの先頭
loTail、添字が変更されていないリンクされたリストの末尾
hiHead、添字が変更されたときのリンクされたリストの先頭
hiTail、添字が変更されたときのリンクされたリストの末尾
注 4 の (e.hash & oldCap) == 0 は、ハッシュ添え字が変更されないことを意味します。この場合、コードは 2 つのパラメーター、loHead と loTail のみを使用し、それらはリンクされたリストを形成します。それ以外の場合、hiHead と hiTailパラメータが使用されます。
実際、e.hash と oldCap が 0 に等しい後のロジックと 0 に等しくないの後のロジックはまったく同じですが、使用される変数が異なります。
0に等しい場合を例として、3–>5–>7の連結リストを処理するには、次のような処理になります。
ノード 3 が最初に処理されます。3、次へ5
1. 注5、loTailは最初はnullなので、loHeadに3を代入します。
2、注 7、loTail に 3 を割り当てます。
次に、ノード 5、e を処理します。5、次へ7
1. 注 6、loTail には値があり、e を loTail.next に割り当てます。つまり 3.next==5 です。
2. 注 7、loTail に 5 を割り当てます。
新しいリンク リストは 3–>5 になり、次にノード 7 が処理されます。処理後のリンク リストの順序は 3–>5–>7 になり、loHead は 3、loTail は 7 になります。リンク リスト内のノードの順序は元のリンク リストと同じであり、JDK1.7 の逆順序ではなくなっていることがわかります。
ここでNote 8のコードを理解するのは簡単ですが、
loTail が null でない限り、新しいテーブルのリンク リストの要素の添字は変更されていないことを意味するため、loHead は新しいテーブルの対応する添字に配置され、loTail の次の文字は null に設定されます。
反対に、hiTail は null ではありません。つまり、新しいテーブルのリンク リスト内の要素の添字は、元の添字に元のテーブルの長さを加えたものでなければなりません。新しいテーブルの対応する添字は hiHead であり、 hiTail の次は null に設定されます。