【java】HashMap展開の仕組み詳細説明

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;
}

コード分​​析:

  1. Simply() メソッドでは、元のテーブルの長さを記録するために oldCap パラメータが定義され、新しいテーブルの長さを記録するために newCap パラメータが定義されています。newCap は oldCap (注 1) の 2 倍の長さであり、拡張子はポイントも2倍になります。

  2. 注 2 は、元のテーブルを循環し、元のテーブルの各リンク リストの各要素を新しいテーブルに配置することです。

  3. 注 3、e.next==null は、リンクされたリストに要素が 1 つだけあることを意味するため、e を新しいテーブルに直接入力します。ここで、e.hash & (newCap - 1) はテーブル内の e の位置を計算します。新しいテーブルと JDK1.7 のindexFor() メソッドは別のものです。

  4. コメント // 順序を保持します。このコメントはソース コードに付属しており、ここで 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 に設定されます。

おすすめ

転載: blog.csdn.net/u011397981/article/details/131462127