新しい理解のHashMap

簡単な紹介

JavaのプログラマのHashMapは、最も頻繁にマッピングするために使用されるハッシュコード結合を格納されたデータ値に応じて、その値に直接ほとんどのケース内に配置され、従って高速アクセスを有することができるデータ処理タイプの(キー、値)スピードが、トラバーサル順序は不明です。HashMapのキーは一つのレコードの最大値は、複数のレコードがnullである可能、nullであることができ、かつスレッドセーフではありませんHashMapのクラスを、そしてSynchronizedMap方法のConcurrentHashMapのコレクションは、スレッドセーフ機能とHashMapを作るに使用することができます。JDK1.8に最適化された赤黒木の導入、展開の最適化を実現するHashMapを基礎となります。その新しい理解は何のJDK1.8 HashMapのは、それを最適化するために行われているものを見てみましょう。

ストレージ構造

HashMapの、それはストレージ構造であることが何であるかを知るHashMapの最初の必要性を調べるには、第二に、それは何をすべきかを把握することができますが、それはそれは達成するために働くか、です。我々は、すべてのHashMapに格納されたデータは、キーに基づいてハッシュ値を格納するハッシュテーブルを使用していることを知っているが、別のキーの間に同じハッシュ値があるかもしれない、これは紛争につながる、ハッシュテーブルの競合を解決するにはJavaのHashMapのは、競合を解決するために、ハッシュ・チェーンアドレス法を使用して、プラスリンクリストの配列は、簡単な言葉の組み合わせで、問題を解決するために、オープンチェーンアドレス法及びアドレス方法することができます。生成されたハッシュ衝突もし記憶されたデータは、配列インデックス、要素のインデックスに対応するリストのデータを取得するために各アレイ素子のためのリスト構造、にあります。ここでは、さらに多くの合理的なハッシュアルゴリズムの設計ならばジッパーはあまりにも長い間表示されたら、必然的に、長すぎるジッパーケースがあるでしょう、それは真剣に行うためのデータ構造のJDK1.8バージョンでのHashMapのパフォーマンスに影響を与えるだろう、問題を考えますさらなる最適化、赤黒木の導入は、リストが長すぎる(デフォルトでは7以上)である場合、図に示すように、リストは、赤黒木に変換されます。

hashMap.png

重要な分野

ハッシュマップは、アクセスキーのハッシュに基づいて、品質と性能HashMapのハッシュアルゴリズムは直接的な関係を有し、その結果均一に分散ハッシュアルゴリズム、ハッシュ衝突の小さい確率が、存在をマッピング抽出効率が高くなります。ハッシュ配列が大きい場合ハッシュ配列が小さい場合もちろん、ハッシュの大きさとの関係の配列は、貧弱なハッシュアルゴリズムが散乱される場合であっても、より良いハッシュアルゴリズムは、より頻繁に発生します衝突ので、我々はよりバランスの取れた値を見つけるために、空間と時間のコストを比較検討する必要があります。

JDK1.8のバージョンは、以前のバージョンの時間、コストとスペース効率を量るたくさんの最適化を行った。データ構造が最適化されているだけでなく、拡張の最適化に加えても大幅にHashMapのパフォーマンスを向上させました。 。どのような具体的な実装を確認するために、ソースコードを介して一緒に私たちをしましょう。

いくつかのプロパティで、より重要なのHashMapで見てみましょう。

//默认的初始容量,必须是2的幂次方.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//所能容纳 key-value 个数的极限,当Map 的size > threshold 会进行扩容 。容量 * 扩容因子
int threshold;

//hashMap最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//HashMap 默认的桶数组的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16

//默认的加载因子.当容量超过 0.75*table.length 扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//HashMap的加载因子,在构造器中指定的.
final float loadFactor;

//链表节点数大于8个链表转红黑树
static final int TREEIFY_THRESHOLD = 8;

//红黑树节点转换链表节点的阈值, 6个节点转
static final int UNTREEIFY_THRESHOLD = 6;

//以Node数组存储元素,长度为2的次幂。
transient Node<K,V>[] table;

// 转红黑树, table的最小长度
static final int MIN_TREEIFY_CAPACITY = 64; 

// 链表节点, 继承自Entry
static class Node<K,V> implements Map.Entry<K,V> {  
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    // ... ...
}

// 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
   
    // ...
}
复制代码

属性のHashMapのは非常によく理解されています。実際には、ここではハッシュテーブルのデフォルトの長さは、バケット16の配列で、長さは2 ^ nはそれでなければならない理由の質問はありますか?

ハッシュ配列の長さが2 ^ nとする理由についてここでは、最初の話。

実際には、JDK1.7またはJDK1.8に計算キーインデックス位置は、ハッシュ・(長さ1)を介して行われるか否か来る計算されます。

私たちは、ハッシュ&( - 1の長さ)と同等のハッシュ%の長さを知っている必要があります。

假如有一个key的哈希值二进制如下:这里我们就只看低位。
hahsCode         0010 0011       ———————转成十进制—————————>        35           
&                                                             %
(length-1)=15:   0000 1111                                length = 16  
-----------------------------------------------------------------------------------------------
             (二进制) 0011  = (十进制)3                            3
复制代码

なぜハッシュ%の長さを計算するインデックス位置、使用するハッシュ・(長さ-1)を計算するために?コンピュータの基礎をなすこと進数である%と比較して計算して記憶され、コンピュータ&下地クローズ操作、作業効率れる高速でなければなりません。

なぜ、長さが2 ^ nはそれでなければなりませんか?

hahsCode         0010 0011                     0010 1111    
&                                     
(length-1)=15:   0000 1111    (length-1) = 13: 0000 1111  
----------------------------------------------------------------------------------------------
                      0011                          1111 
复制代码
hahsCode         0010 1110                     1110 1100  
&                                     
(length-1)=13:   0000 0101    (length-1) = 13: 0000 0101  
----------------------------------------------------------------------------------------------
                      0100                          0100 
复制代码

実際には、我々が見ることができるハッシュ配列の長さが2である場合^ N、長さ - インデックスが完全にハッシュ値の低い値に依存し、そして衝突の確率になるように1つのバイナリコードは、1に全て設定されています2以下のn番目の電力容量の確率を低下させるために、インデックス位置は限り均一な分布のハッシュ値と、次に衝突の確率ははるかに低くなり、低いハッシュ値に完全に依存し、したがって、長さが2のn倍より良いパーティー。

2のn乗の長さは、拡張することも便利であるときに、第2は、拡張アルゴリズム最適化された方法でJDK1.8も非常に巧妙です。これは、時間展開方法で言及されます。

達成するための機能

インデックス位置を決定します

私たちは確かにこの位置HashMapの要素が内部に均等に可能な限り、と試しに配布することを期待しているかどうかは、HashMapのデータ構造は配列やリンクリストの組み合わせである前に言った、ハッシュ・バケットの配列の位置を特定する必要がある、見つける、削除、追加しますハッシュアルゴリズムを使用するときに我々は、この位置を見つけたとき、私たちはすぐに我々が何をしたいの対応する要素の位置を知ることができるので、各位置での要素の数は一つだけであること、そして、我々は大幅にクエリの効率を改善し、リストクエリを横断する必要はありません。

ハッシュバケットアレイのサイズは常に2 ^ n個になる場合tableSizeFor()このメソッドは、初期化のHashMap()を確保することです。

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;
        
  /**
  假如现在传入的参数cap = 3
  那 n = 2 对应的二进制 为 10 
  n  = n | n>>>1  10|01  得到 11
  ....
  ....
  n = 11(二进制)  = (10进制) 3 
  最后return 返回的是4
  */
}
复制代码
//JDK1.8的Hash算法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// JDK 1.7的Hash算法
 static final int hash(int h) {
    h ^= k.hashCode(); 
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
 }

//索引位置
index = hash & (length-1);

//JDK1.7 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
//JDK 1.8 简化了hash函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算。
复制代码

)JDK1.8、ハッシュコード(による最適化アルゴリズム高い計算の実装では、高下位16ビットの16ビットXORが実装:(H = k.hashCode())^(H >>> 16)、主に考慮すべき速度、効率および品質から、それがJDK1.7と比較され、JDK1.8は、ハッシュ関数障害の数を減らすには、ハッシュアルゴリズムを最適化することが考えられます。小容量のHashMapの時間にそうするだけでなく、多くのオーバーヘッドがない一方で、考慮に関与しているコンピューティングハッシュビットのレベルを取って確実にするために。

假如有一个key的哈希值二进制如下
hahsCode               0000 0000 0011 0011 0111 1010 1000 1011

hahsCode>>>16          0000 0000 0000 0000 0000 0000 0011 0011
 ———————————————————————————————————————————————————————————————
位或^运算               0000 0000 0011 0011 0111 1010 1011 1000
 &
HashMap.size()-1       0000 0000 0000 0000 0000 0000 0000 1111
 ———————————————————————————————————————————————————————————————
                       0000 0000 0000 0000 0000 0000 0000 1000 转成十进制是 8
复制代码

putメソッド

フローチャートを見つけるために、インターネットから、私が直接、非常に良い感じで引き継ぐ、とちょっと....絵が比較的明確です。フロー・チャートを見て、理解するために参照するソースコードと組み合わせます。画像の名前

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)
        
         //如果哈希桶数组为空,对其进行初始化。默认的桶数组大小为16
        n = (tab = resize()).length;
            
        //如果桶数组不为空,得到计算key的索引位置,判断此索引所在位置是否已经被占用了。
        if ((p = tab[i = (n - 1) & hash]) == null)
        
        //如果没有被占用,那就封装成Node节点,放入哈希桶数组中。
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果要插入的Node节点已经存在,那就将旧的Node替换。
            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) {d
                    
                        p.next = newNode(hash, key, value, null);
                        
                        //如果链表的长度大于7,就把节点转成树节点
                        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;
    }
复制代码

getメソッド

単純に置いてもよい方法に関して方法を取得し、ソースコードを見て理解することができます。ADOは、直接コードでそれを見ることです。

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {

        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //哈希桶数组不为空,且根据传入的key计算出索引位置的Node不为空。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果计算出来的第一个哈希桶位置的Node就是要找的Node节点,直接返回。
            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;
  }
复制代码

膨張機構(リサイズ)

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果老的HashMap容量不为空
        if (oldCap > 0) {
            //如果容量大于或者等于这个扩容的临界点
            if (oldCap >= MAXIMUM_CAPACITY) {
            //修改阈值为2^31-1
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }  
            // 没超过最大值,就扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果老的容量为0, 老的阈值大于0, 是因为初始容量没有被放入阈值,则将新表的容量设置为老表的阈值
        else if (oldThr > 0) 
            newCap = oldThr;
        else {
        //老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量,将阈值和容量设置为默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 计算新的resize上限
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
       // 将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量。将旧Hash桶中的元素,移动到新的Hash数组中。
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 如果原来的容量不为空,把每个bucket都移动到新的buckets中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                
                if ((e = oldTab[j]) != null) {
                // 将老表的节点设置为空, 以便垃圾收集器回收空间
                    oldTab[j] = null;
                    
                    //哈希桶位置只有一个节点。rehash之后再放到newTab里面去
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //如果是红黑树节点,则进行红黑树的重hash分布
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //如果是普通的链表节点,则进行普通的重hash分布
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                    //如果要移动节点的hash值与老的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                           if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                    //如果e的hash值与老表的容量进行与运算不为0,则扩容后的索引位置为:老表的索引位置+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
复制代码

ソースコードでは、これは(e.hash&oldCap)== 0は、これを理解する方法を、以下での外観を聞かせて言っています

假设扩容之前 数组大小为16
假如有两个key:
key1(hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1011 1000
key2(hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1010 1000
          &
    length-1 = 15    0000 0000 0000 0000 0000 0000 0000 1111
——————————————————————————————————————————————————————————————————
                                                  key1: 1000 转成十进制 8
                                                  key2: 1000 转成十进制 8
                                                  
哈希冲突的两个key,在扩容到32之后
key1(key的hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1011 1000
key2(key的hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1010 1000
           &
         length-1 = 31    0000 0000 0000 0000 0000 0000 0001 1111
——————————————————————————————————————————————————————————————————
                                                   key1:   1 1000 转乘二进制 24=16+8
                                                   key2:   0 1000 转乘二进制 8
复制代码

私たちが見ることができる上に、拡張後の位置によって同じ位置に元の2つのキーのいずれかを元の位置にある、またはoldCapで+元の位置を通して。この排除ちょうど1か0ビットのようである新しいオリジナルのハッシュ値を見て、JDK1.7を達成するようにハッシュを再計算する必要性は、その後、インデックスは、「オリジナルと1で、その後、インデックスが変更されていない0でありますインデックス+ oldCap」。HashMapの容量はn個のパワーに2でなければならない理由だけでなく、より完全に説明しています。

JDK1.8この設計は0または1でさえこのようにする前に、プロセスのサイズを変更し、ランダム考えることができているハッシュ値を再計算するための時間の必要性を排除すると同時に、新たに1ビットのためにだけではなく、確かに非常に賢いですノードの競合は、新しいバケットに分散されています。

hashMap_resize.png

概要

  1. 拡張は、特に高価な操作性能であるので、我々は、HashMapの時間に使用する場合、値の初期化にマップの推定サイズは、実質上、頻繁な拡張マップを避けます。
  2. HashMapのは、スレッドセーフではありません、のConcurrentHashMapまたはSynchronizedMapを使用して、同時実行環境でHashMapを使用していません
  3. 負荷率を変更することができ、それは1よりも大きくすることができ、容易に変更されるべきではないが推奨されます。

おすすめ

転載: juejin.im/post/5e1709d9f265da3df61ff260