重新認(rèn)識JDK1.8中的HashMap

簡介

HashMap是Java程序員使用頻率最高的用于映射(鍵犬辰、值對)處理的數(shù)據(jù)類型浑吟,它根據(jù)鍵的hashCode值存儲數(shù)據(jù),大多數(shù)情況下可以直接定位到它的值赶盔,因而具有很快的訪問速度历恐,但遍歷順序卻是不確定的寸癌。 HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null弱贼,且HashMap不是線程安全的類,可以使用ConcurrentHashMap和Collections的SynchronizedMap方法使HashMap具有線程安全的能力磷蛹。在JDK1.8中對HashMap底層的實現(xiàn)進(jìn)行了優(yōu)化吮旅,引入紅黑樹、擴(kuò)容優(yōu)化等味咳。那就重新認(rèn)識一下JDK1.8的HashMap庇勃,來看看對其做了什么優(yōu)化。

存儲結(jié)構(gòu)

要搞清楚HashMap槽驶,首先需要知道HashMap是什么责嚷,即它的存儲結(jié)構(gòu);其次弄明白它能干什么掂铐,即它的功能如何實現(xiàn)罕拂。我們都知道HashMap使用哈希表來存儲的數(shù)據(jù)的揍异,根據(jù)key的哈希值進(jìn)行存儲的,但是不同的key之間可能存在相同的哈希值爆班,這樣就會產(chǎn)生沖突衷掷;哈希表為解決沖突,可以采用開放地址法和鏈地址法等來解決問題柿菩,Java中的HashMap采用了鏈地址法來解決哈希沖突戚嗅,簡單來說就是數(shù)組加鏈表的結(jié)合。在每個數(shù)組元素上都一個鏈表結(jié)構(gòu)枢舶,當(dāng)存放數(shù)據(jù)的時候如果產(chǎn)生了哈希沖突懦胞,先得到數(shù)組下標(biāo),把數(shù)據(jù)放在對應(yīng)下標(biāo)元素的鏈表上凉泄。這里我們思考一個問題医瘫,即使哈希算法設(shè)計的再合理,也免不了會出現(xiàn)拉鏈過長的情況旧困,一旦出現(xiàn)拉鏈過長醇份,則會嚴(yán)重影響HashMap的性能,在JDK1.8版本中吼具,對數(shù)據(jù)結(jié)構(gòu)做了進(jìn)一步的優(yōu)化僚纷,引入了紅黑樹;當(dāng)鏈表長度太長(默認(rèn)超過7)時拗盒,鏈表就轉(zhuǎn)換為紅黑樹怖竭,如下圖所示。

hashMap.png

重要字段

HashMap是根據(jù)key的哈希值進(jìn)行存取的陡蝇,那HashMap的性能和哈希算法的好壞有著直接的關(guān)系痊臭,哈希算法計算結(jié)果越分散均勻,哈希碰撞的概率就越小登夫,map的存取效率就會越高广匙。當(dāng)然,也和哈希數(shù)組的大小有關(guān)系恼策,如果哈希數(shù)組很大鸦致,即使較差的哈希算法也會比較分散,如果哈希數(shù)組較小涣楷,即使較好的哈希算法也會出現(xiàn)較多的碰撞分唾,所以就需要權(quán)衡空間和時間成本,找到比較平衡的值狮斗。

JDK1.8版本也是權(quán)衡了時間绽乔、空間成本以及效率,對之前的版本做出了很多優(yōu)化碳褒;不僅對數(shù)據(jù)結(jié)構(gòu)進(jìn)行了優(yōu)化折砸,除此之外還對擴(kuò)容進(jìn)行的優(yōu)化看疗,大大的提高的HashMap的性能。下面我們通過源碼來一起看一下具體的實現(xiàn)鞍爱。

我們來看一下HashMap中比較重要的幾個屬性鹃觉。

//默認(rèn)的初始容量,必須是2的冪次方.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//所能容納 key-value 個數(shù)的極限睹逃,當(dāng)Map 的size > threshold 會進(jìn)行擴(kuò)容 盗扇。容量 * 擴(kuò)容因子
int threshold;

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

//HashMap 默認(rèn)的桶數(shù)組的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16

//默認(rèn)的加載因子.當(dāng)容量超過 0.75*table.length 擴(kuò)容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//HashMap的加載因子,在構(gòu)造器中指定的.
final float loadFactor;

//鏈表節(jié)點(diǎn)數(shù)大于8個鏈表轉(zhuǎn)紅黑樹
static final int TREEIFY_THRESHOLD = 8;

//紅黑樹節(jié)點(diǎn)轉(zhuǎn)換鏈表節(jié)點(diǎn)的閾值, 6個節(jié)點(diǎn)轉(zhuǎn)
static final int UNTREEIFY_THRESHOLD = 6;

//以Node數(shù)組存儲元素沉填,長度為2的次冪疗隶。
transient Node<K,V>[] table;

// 轉(zhuǎn)紅黑樹, table的最小長度
static final int MIN_TREEIFY_CAPACITY = 64; 

// 鏈表節(jié)點(diǎn), 繼承自Entry
static class Node<K,V> implements Map.Entry<K,V> {  
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    // ... ...
}

// 紅黑樹節(jié)點(diǎn)
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中的屬性還是比較好理解的。其實到這里會有一個疑問翼闹,為什么默認(rèn)的哈希桶數(shù)組table的長度為16斑鼻,而且長度必須為2的n次方呢?

這里我們先說一下為什么哈希數(shù)組的長度是2的n次方猎荠。

其實不管是在JDK1.7還是JDK1.8中坚弱,計算key索引位置都是通過hash & (length-1)計算得來的。

我們應(yīng)該知道 hash % length 等價于 hash & (length - 1)关摇。

假如有一個key的哈希值二進(jìn)制如下:這里我們就只看低位荒叶。
hahsCode         0010 0011       ———————轉(zhuǎn)成十進(jìn)制—————————>        35           
&                                                             %
(length-1)=15:   0000 1111                                length = 16  
-----------------------------------------------------------------------------------------------
             (二進(jìn)制) 0011  = (十進(jìn)制)3                            3

為什么不用 hash % length 計算索引位,要使用 hash & (length -1)來計算呢输虱?計算機(jī)底層是二進(jìn)制數(shù)進(jìn)行計算和存儲些楣,&是接近計算機(jī)底層運(yùn)算,相比于% 運(yùn)算效率上應(yīng)該會快宪睹。

那為什么length必須是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 

其實我們可以發(fā)現(xiàn),當(dāng)哈希數(shù)組的長度為2的n次方時亭病,length - 1的二進(jìn)制碼全都是1鹅很,這樣的話索引的位置,完全依賴于hash值的低位值命贴,而且產(chǎn)生沖突的幾率要比容量不是2的n次方的概率要低道宅,索引位就完全依賴于哈希值的低位,所以只要哈希值分布均勻胸蛛,那產(chǎn)生沖突的概率就會低很多,故而length是2的n次方更優(yōu)樱报。

其次葬项,當(dāng)length為2的n次方時,也方便做擴(kuò)容迹蛤,JDK1.8在擴(kuò)容算法上也進(jìn)行了優(yōu)化民珍,使用的方法也非常巧妙襟士。會在擴(kuò)容方法的時候講到。

功能實現(xiàn)

確定索引位置

不管是增加嚷量、刪除陋桂、查找,都需要定位到哈希桶的數(shù)組位置蝶溶,前面也說過HashMap的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合嗜历,所以我們當(dāng)然希望這個HashMap里面的元素位置盡量分布均勻些压状,盡量使得每個位置上的元素數(shù)量只有一個痛阻,那么當(dāng)我們用hash算法求得這個位置的時候芒篷,馬上就可以知道對應(yīng)位置的元素就是我們要的膏蚓,不用再遍歷鏈表查詢趣竣,大大優(yōu)化了查詢效率推沸。

tableSizeFor()這個方法蔚携,就是保證在HashMap()進(jìn)行初始化的時候房维,哈希桶數(shù)組的大小永遠(yuǎn)是2^n傻粘。

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;
        
  /**
  假如現(xiàn)在傳入的參數(shù)cap = 3
  那 n = 2 對應(yīng)的二進(jìn)制 為 10 
  n  = n | n>>>1  10|01  得到 11
  ....
  ....
  n = 11(二進(jìn)制)  = (10進(jìn)制) 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次位運(yùn)算 + 5次異或運(yùn)算(9次擾動)
//JDK 1.8 簡化了hash函數(shù) = 只做了2次擾動 = 1次位運(yùn)算 + 1次異或運(yùn)算每窖。

在JDK1.8的實現(xiàn)中,優(yōu)化了高位運(yùn)算的算法弦悉,通過hashCode()的高16位異或低16位實現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16)窒典,主要是從速度、功效警绩、質(zhì)量來考慮的崇败,相比于JDK1.7來說,JDK1.8降低了哈希函數(shù)擾動的次數(shù)肩祥,也算是優(yōu)化了hash算法后室。這么做可以在HashMap容量較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中混狠,同時不會有太大的開銷岸霹。

假如有一個key的哈希值二進(jìn)制如下
hahsCode               0000 0000 0011 0011 0111 1010 1000 1011

hahsCode>>>16          0000 0000 0000 0000 0000 0000 0011 0011
 ———————————————————————————————————————————————————————————————
位或^運(yùn)算               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 轉(zhuǎn)成十進(jìn)制是 8

put方法

從網(wǎng)上找到了一個流程圖,感覺很不錯将饺,就直接拿過來用了贡避,嘿嘿....畫的也是比較清楚的∮杌。看著流程圖刮吧,再結(jié)合源碼一看就明白。
<img src="https://user-gold-cdn.xitu.io/2019/12/30/16f547c76710e6d7?w=1716&h=1360&f=png&s=314628" width = "550" height = "450" alt="圖片名稱" align="center">

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        //判斷哈希桶數(shù)組是否為空掖蛤。
        if ((tab = table) == null || (n = tab.length) == 0)
        
         //如果哈希桶數(shù)組為空杀捻,對其進(jìn)行初始化。默認(rèn)的桶數(shù)組大小為16
        n = (tab = resize()).length;
            
        //如果桶數(shù)組不為空蚓庭,得到計算key的索引位置致讥,判斷此索引所在位置是否已經(jīng)被占用了仅仆。
        if ((p = tab[i = (n - 1) & hash]) == null)
        
        //如果沒有被占用,那就封裝成Node節(jié)點(diǎn)垢袱,放入哈希桶數(shù)組中墓拜。
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果要插入的Node節(jié)點(diǎn)已經(jīng)存在,那就將舊的Node替換请契。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果不存在咳榜,那就插入,判斷插入的節(jié)點(diǎn)是不是樹節(jié)點(diǎn)姚糊。
            else if (p instanceof TreeNode)
            
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                
            else {
            //如果是普通節(jié)點(diǎn)贿衍,循環(huán)哈希桶對應(yīng)的鏈表,將節(jié)點(diǎn)插入到鏈表末尾
                for (int binCount = 0; ; ++binCount) {
                    
                    if ((e = p.next) == null) {d
                    
                        p.next = newNode(hash, key, value, null);
                        
                        //如果鏈表的長度大于7,就把節(jié)點(diǎn)轉(zhuǎn)成樹節(jié)點(diǎn)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果鏈表中節(jié)點(diǎn)已經(jīng)存在救恨,那就將舊的節(jié)點(diǎn)替換贸辈。
                    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;
        //如果超過了擴(kuò)容的臨界點(diǎn),就進(jìn)行擴(kuò)容肠槽。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

get方法

get方法相對于put方法可能簡單一點(diǎn)擎淤,通過源碼一看就能明白。廢話不多說秸仙,直接上代碼看一下吧嘴拢。

 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;
        //哈希桶數(shù)組不為空,且根據(jù)傳入的key計算出索引位置的Node不為空寂纪。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果計算出來的第一個哈希桶位置的Node就是要找的Node節(jié)點(diǎn)席吴,直接返回。
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            if ((e = first.next) != null) {
                //如果是樹節(jié)點(diǎn)捞蛋,直接通過樹節(jié)點(diǎn)的方式查找孝冒。
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //循環(huán)遍歷哈希桶所在的鏈表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
  }

擴(kuò)容機(jī)制(resize)

我個人認(rèn)為HashMap的擴(kuò)容算法是整個HashMap中的精髓部分,使用的方法也十分巧妙拟杉,不得不佩服庄涡,大佬還是大佬。

final Node<K,V>[] resize() {
        //將當(dāng)前的table 暫存的 oldTab中進(jìn)行操作
        Node<K,V>[] oldTab = table;
        //當(dāng)前的哈希桶數(shù)組大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //當(dāng)前的擴(kuò)容臨界值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果當(dāng)前哈希桶數(shù)組不為空
        if (oldCap > 0) {
          //如果容量超過最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
            //修改閾值為2^31-1
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }  
            // 沒超過最大值搬设,就擴(kuò)充為原來的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) 
          // 當(dāng)使用 new HashMap(int initialCapacity) 初始化后穴店,第一次 put 的時候
            newCap = oldThr; 
        else {
          //如果代碼到這里,說明hashMap還沒有進(jìn)行初始化, 對應(yīng) new HashMap()拿穴;
            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);
        }
       // 將當(dāng)前閾值設(shè)置為剛計算出來的新的閾值泣洞,定義新表,容量為剛計算出來的新容量默色。將舊Hash桶中的元素斜棚,移動到新的Hash數(shù)組中。
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
       /**
        如果只是對HashMap的初始化來說该窗,到這里已經(jīng)結(jié)束了
        下面代碼是將老的數(shù)組中的數(shù)據(jù)弟蚀,移動到擴(kuò)容以后的哈希桶數(shù)組中。
       **/
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                
                if ((e = oldTab[j]) != null) {
                // 將老表的節(jié)點(diǎn)設(shè)置為空, 以便垃圾收集器回收
                    oldTab[j] = null;
                    
              //判斷哈希桶位置是否只有一個節(jié)點(diǎn)酗失。如果這個位置只有一個節(jié)點(diǎn)义钉,那就將這個節(jié)點(diǎn)在擴(kuò)容以后的哈希桶數(shù)組中重新計算位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //如果是紅黑樹節(jié)點(diǎn),則進(jìn)行紅黑樹的重hash分布
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    //如果是普通的鏈表節(jié)點(diǎn)规肴,則需要將這個位置上的所有節(jié)點(diǎn)重新在新的哈希桶數(shù)組中計算位置捶闸。
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
          //如果要移動節(jié)點(diǎn)的hash值與老的容量進(jìn)行 "&" 運(yùn)算,如果結(jié)果為0拖刃,那在擴(kuò)容以后的數(shù)組中删壮,還是同樣的位置。
                           if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                    //如果運(yùn)算不為0,則擴(kuò)容后的索引位置為:老表的索引位置+擴(kuò)容之前的
                            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,怎么理解這個呢兑牡,我們通過下面的來看一下央碟。

假設(shè)擴(kuò)容之前 數(shù)組大小為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 轉(zhuǎn)成十進(jìn)制 8
                                                  key2: 1000 轉(zhuǎn)成十進(jìn)制 8
                                                  
哈希沖突的兩個key,在擴(kuò)容到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 轉(zhuǎn)乘二進(jìn)制 24=16+8
                                                   key2:   0 1000 轉(zhuǎn)乘二進(jìn)制 8

我們可以發(fā)現(xiàn)均函,代碼中利用的是 hash & oldCap(key的哈希值 & 擴(kuò)容之前的哈希桶長度)亿虽,而不是利用hash & newCap- 1 ,為什么要這樣做呢苞也?

通過上面的情況我們應(yīng)該可以看出洛勉,原本沖突的兩個key,在擴(kuò)容以后的位置是由新增的那一個bit位來決定的如迟。所以直接用 hash & 擴(kuò)容之前的容量來判斷新增的那個bit位是0 還是 1 收毫,就可以知道是在原來的位置上,還是在 原來位置+oldCap 位置上殷勘。

哈希沖突的兩個key此再,在擴(kuò)容到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
           &
         oldCap=16        0000 0000 0000 0000 0000 0000 0001 0000
--------------------------------------------------------------------
                                                     key1: 1 0000
                                                     key2: 0 0000

通過以上,我們可以看到劳吠,原來在同一個位置上的兩個key引润,通過擴(kuò)容之后的位置要不在原來的位置上,要不在oldCap+原位置上痒玩。這樣不需要像JDK1.7的實現(xiàn)那樣重新計算hash淳附,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變蠢古,是1的話索引變成“原索引+oldCap”奴曙。同時也是更加充分的說明了,為什么HashMap的容量必須是2的n次方了草讶。

知道HashTable的同學(xué)應(yīng)該也知道洽糟,HashTable默認(rèn)的容量是11,每次擴(kuò)容之后的容量是length*2+1。HashTable在設(shè)計上坤溃,想通過保證哈希桶數(shù)組為質(zhì)數(shù)拍霜,來盡可能保證哈希散列均勻。這里也就不對HashTable的容量為什么是質(zhì)數(shù)進(jìn)行展開薪介,感興趣的同學(xué)可以查一下資料祠饺,為什么對質(zhì)數(shù)求模會比對合數(shù)求模的哈希散列效果要好,可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159汁政。HashMap雖然也是想通過類似HashTable中的哈希算法那樣(通過對質(zhì)數(shù)求模來)降低哈希沖突道偷,顯然HashMap中的哈希算法要比HashTable中哈希算法要優(yōu)秀很多。

HashMap保證哈希數(shù)組的大小為2的n次方记劈,這個設(shè)計確實非常的巧妙勺鸦,利用2的n次方 - 1 二進(jìn)制碼全為1這個特性,在擴(kuò)容的時候目木,既省去了重新計算hash值的時間换途,而且同時,由于新增的1bit是0還是1可以認(rèn)為是隨機(jī)的嘶窄,因此resize的過程怀跛,均勻的把之前的沖突的節(jié)點(diǎn)分散到新的bucket了。

hashMap_resize.png

總結(jié)

  1. 擴(kuò)容是一個特別耗性能的操作柄冲,所以當(dāng)我們在使用HashMap的時候吻谋,估算map的大小,初始化的時候給一個大致的數(shù)值现横,避免map進(jìn)行頻繁的擴(kuò)容漓拾。
  2. HashMap是線程不安全的,不要在并發(fā)的環(huán)境中使用HashMap戒祠,使用ConcurrentHashMap或者SynchronizedMap
  3. 負(fù)載因子是可以修改的骇两,也可以大于1,但是建議不要輕易修改姜盈。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末低千,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子馏颂,更是在濱河造成了極大的恐慌示血,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件救拉,死亡現(xiàn)場離奇詭異难审,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)亿絮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門告喊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來麸拄,“玉大人,你說我怎么就攤上這事黔姜÷G校” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵地淀,是天一觀的道長失球。 經(jīng)常有香客問我,道長帮毁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任豺撑,我火速辦了婚禮烈疚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘聪轿。我一直安慰自己爷肝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布陆错。 她就那樣靜靜地躺著灯抛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪音瓷。 梳的紋絲不亂的頭發(fā)上对嚼,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音绳慎,去河邊找鬼纵竖。 笑死,一個胖子當(dāng)著我的面吹牛杏愤,可吹牛的內(nèi)容都是我干的靡砌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼珊楼,長吁一口氣:“原來是場噩夢啊……” “哼通殃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起厕宗,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤画舌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后媳瞪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骗炉,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年蛇受,在試婚紗的時候發(fā)現(xiàn)自己被綠了句葵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖乍丈,靈堂內(nèi)的尸體忽然破棺而出剂碴,到底是詐尸還是另有隱情,我是刑警寧澤轻专,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布忆矛,位于F島的核電站,受9級特大地震影響请垛,放射性物質(zhì)發(fā)生泄漏催训。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一宗收、第九天 我趴在偏房一處隱蔽的房頂上張望漫拭。 院中可真熱鬧,春花似錦混稽、人聲如沸采驻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽礼旅。三九已至,卻和暖如春洽洁,著一層夾襖步出監(jiān)牢的瞬間痘系,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工诡挂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碎浇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓璃俗,卻偏偏與公主長得像奴璃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子城豁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容