HashMapソースコードでエレガントなデザインについて話す

1. HashMapコンストラクター
HashMapは、HashMapオブジェクトを作成するための3つのコンストラクターを提供します。
1.パラメーターなしコンストラクターpublicHashMap():パラメーターなしコンストラクターによって作成されたハッシュマップオブジェクトのデフォルトの容量は16で、デフォルトの負荷係数は0.75です。
2.パラメーターコンストラクターpublicHashMap(int initialCapacity、float loadFactor):このコンストラクターを使用して、ハッシュマップの初期容量と負荷係数を指定できますが、ハッシュマップの下部では、必ずしも渡した容量に初期化されるとは限りません。で、ただし、渡された値以上の2の最小乗に初期化されます。たとえば、17を渡した場合、ハッシュマップは32(2 ^ 5)に初期化されます。では、ハッシュマップはどのようにして数値以上の2の最小累乗を効率的に計算するのでしょうか?ソースコードは次のとおりです。

static final int tableSizeFor(int cap) {
    
    
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  }

その設計は非常に巧妙であると言えます。基本的な考え方は、2進数の下位ビットがすべて1の場合、この数値+1は2の累乗でなければならないということです。例を見てみましょう:
ここに画像の説明を挿入

ご覧のとおり、その計算プロセスは次のとおりです。最初に指定した数値キャップから1を減算し(1を減算する理由は、キャップが正確に2の累乗である場合、正しく計算できるためです)、次にcap-1符号なし右シフトは1、2、4、8、および16ビット(合計で正確に31ビット)であり、各シフト後に前の数値とビット単位のOR演算を実行します。この演算により、最終結果の下位ビットはすべて1になります。次に、最後に結果に1を加算すると、2の累乗が得られます。
3.別のパラメーター化されたコンストラクターはパラメーター化されたコンストラクターpublicHashMap(int initialCapacity)です。このコンストラクターと前のコンストラクターの唯一の違いは、負荷係数を指定できないことです。

2.HashMap挿入メカニズム
1.メソッドのソースコードを挿入します

public V put(K key, V value) {
    
    
        return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化桶数组 table, table 被延迟到插入新数据时再进行初始化
        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;
            //如果hash相等,并且equals方法返回true,这说明key相同,此时直接替换value即可,并且返回原值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
             //如果第一个节点是树节点,则调用putTreeVal方法,将当前值放入红黑树中
            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);
                        //判断链表中节点树是否超多了阈值8,如果超过了则将链表转换为红黑树(当然不一定会转换,treeifyBin方法中还有判断)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果在链表中找到,完全相同的key,则直接替换value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e!=null说明只是遍历到中间就break了,该种情况就是在链表中找到了完全相等的key,该if块中就是对value的替换操作
            if (e != null) {
    
     // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //加入value之后,更新size,如果超过阈值,则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

2.フローチャートを挿入します
ここに画像の説明を挿入
(1)kvを配置するときは、最初にhash()メソッドを呼び出してキーのハッシュコードを計算しますが、ハッシュマップでは、単にキーのハッシュコードを呼び出してハッシュコードを取得するのではなく、外乱関数は、ハッシュの衝突を減らすためにも使用されます。ソースコードは次のとおりです。

static final int hash(Object key) {
    
    
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

ソースコードからわかるように、最終的なハッシュ値は、元のハッシュコードと元のハッシュコードを16ビット右にシフトして得られた値に対するXOR演算の結果です。16は32のちょうど半分であるため、ハッシュマップはハッシュコードの上位ビットを下位ビットに移動し、排他的論理和演算によって上位ビットを下位ビットに拡散して、ハッシュの衝突を減らします。競合を減らすことができる理由については、ハッシュメソッドに関する作成者のコメントを見ることができます。

key.hashCode()を計算し、ハッシュの上位ビットを下位ビットに拡散(XOR)します。テーブルは2の累乗のマスキングを使用しているため、現在のマスクよりビット単位でのみ変化するハッシュのセットは常に衝突します。(既知の例の中には、小さなテーブルに連続する整数を保持するFloatキーのセットがあります。)そこで、上位ビットの影響を下方に分散する変換を適用します。ビット拡散の速度、有用性、および品質の間にはトレードオフがあります。多くの一般的なハッシュのセットはすでに合理的に分散されており(したがって、拡散のメリットはありません)、ビン内の衝突の大規模なセットを処理するためにツリーを使用するため、体系的な損失を減らすために、可能な限り安価な方法でいくつかのシフトされたビットをXORします。また、テーブルの境界のためにインデックス計算で使用されない最上位ビットの影響を組み込むこともできます。

コメントから、著者が高から低に普及した理由は、ハッシュマップがバケット添え字を計算するとき、計算方法はhash&n-1、nは2の累乗であるため、hash&n-1が取り出されただけであると結論付けることができます。たとえば、nが16の場合、hash&n-1はハッシュの下位4ビットを取り出します。複数のハッシュの下位4ビットがまったく同じである場合、ハッシュが同じであっても、常に衝突(競合)が発生します。違います。したがって、上位ビットは下位ビットに分散され、上位ビットも計算に関与するため、競合が減少し、データストレージがよりハッシュ化されます。

(2)ハッシュを計算した後、putValメソッドを呼び出してKey-Valueを格納します。putValメソッドでは、最初にテーブルが初期化されているかどうかを判断する必要があります(ハッシュマップは遅延初期化され、オブジェクトの作成時にテーブルは初期化されないため)。テーブルが初期化されていない場合は、resizeメソッドを使用して展開します。

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

(3)(n-1)&hashを介して現在のキーが配置されているバケット添え字を計算します。現在のテーブルの現在の添え字にデータが保存されていない場合は、リンクリストノードを作成し、現在のkvを添え字に直接保存します。ロケーション。

if ((p = tab[i = (n - 1) & hash]) == null)
     tab[i] = newNode(hash, key, value, null);

(4)テーブルの添え字にすでにデータがある場合は、最初に現在のキーが添え字に格納されているキーと正確に等しいかどうかを判断します。等しい場合は、値を直接置き換えて元の値を返します。それ以外の場合は、引き続きトラバースします。リンクリストまたは赤黒木に保存します。

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
      e = p;

(5)現在の添え字のノードがツリーノードの場合、赤黒木に直接格納されます。

else if (p instanceof TreeNode)
         e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

(6)赤黒木でない場合は、リンクリストをトラバースします。リンクリストをトラバースする過程で等しいキーが見つかった場合は、値を置き換えます。等しいキーがない場合は、ノードをに格納します。リンクリストの終わり(テールはjdk8で使用されます)。補間)、現在のリンクリストのノードツリーがしきい値8を超えているかどうかを確認します。8を超えると、リンクリストは赤黒木に変換されます。 treeifyBinメソッドを呼び出します。

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

(7)データの保存後、現在のハッシュマップのサイズが拡張しきい値Cap * load_factを超えているかどうかを判断する必要があります。しきい値より大きい場合は、resize()メソッドを呼び出して拡張します。

f (++size > threshold)
       resize();

拡張後のHashMapの容量は、元の容量の2倍です。基本的なメカニズムは、2倍の容量のテーブルを作成し、データを新しいハッシュテーブルにダンプして、新しいハッシュテーブルを返すことです。jdk1.7との違いは、jdk1.8のマルチダンプが最適化されており、バケットの添え字を再計算する必要がないことです。実装のソースコードは次のとおりです。
ここに画像の説明を挿入
ソースコードから、次のことがわかります。キーハッシュと元の容量oldCapのビット単位のAND演算結果が0の場合、展開前のバケット添え字は展開後のバケット添え字と等しくなります。それ以外の場合、展開後のバケット添え字は元の添え字にoldCapを加えたものになります。使用される基本原則は次のように要約されます。

1.数値mと2の累乗nのビット単位のAND演算が0に等しくない場合、次のようになります。m&(n 2-1)= m&(n-1)+ n
理解:2の累乗2進数nの場合、2進数の1ビットのみが1(k番目のビットが1であると想定)であり、他のビットはすべて0です。数値mおよびnのビット単位のAND演算の結果が0の場合、これはmの2進数kビットは0でなければならないため、mの最初のnビットと最初のn-1ビットで表される値は等しくなければなりません。
2.数値mと2の累乗の数値nが、0に等しいビット単位のAND演算の対象となる場合、次のようになります。m&(n
2-1)= m&(n-1)
理解:2の累乗の数値n 、バイナリシステムでは、1ビットのみが1(k番目のビットが1であると仮定)であり、他のビットはすべて0です。数値mおよびnのビット単位のAND演算の結果が0でない場合、それは次のことを意味します。 mのバイナリシステムのk番目のビット1でなければなりません。その場合、mの最初のnビットと最初のn-1ビットで表される値の差は、k番目の位置で1で表される数とまったく同じです。 2番目の数値は正確にnです。

回路図:
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/qq_40400960/article/details/114048299