HashMap 源碼

HashMap也是我們使用非常多的Collection,它是基于哈希表的 Map 接口的實(shí)現(xiàn),以key-value的形式存在。在HashMap中,key-value總是會(huì)當(dāng)做一個(gè)整體來(lái)處理,系統(tǒng)會(huì)根據(jù)hash算法來(lái)來(lái)計(jì)算key-value的存儲(chǔ)位置,我們總是可以通過(guò)key快速地存、取value。下面就來(lái)分析HashMap的存取。

一. 定義

HashMap實(shí)現(xiàn)了Map接口,繼承AbstractMap。這是使用了模板方法模式,其中Map接口定義了鍵映射到值的規(guī)則,而AbstractMap類提供 Map 接口的骨干實(shí)現(xiàn),為 Map 的實(shí)現(xiàn)類提供了一些通用方法。

public class HashMap<K, V> extends AbstractMap<K, V>
                           implements Map<K, V>, Cloneable, Serializable

二. 構(gòu)造函數(shù)

HashMap 有多個(gè)構(gòu)造函數(shù),在處理 threshold 和 loadFactor 時(shí)有所不同,所以在 resize() 方法在初始化時(shí)會(huì)對(duì)不同的情況采用相應(yīng)的處理邏輯。


 /*
  * 構(gòu)造一個(gè)具有默認(rèn)初始容量 (16) 和默認(rèn)加載因子 (0.75) 的空 HashMap
  * 這個(gè)方法沒(méi)有初始化 threshold 和 loadFactor
  * 所以此時(shí)兩個(gè)變量的值都為零,與其他構(gòu)造函數(shù)不一樣
  */
 public HashMap() {}
 // 構(gòu)造一個(gè)帶指定初始容量和默認(rèn)加載因子 (0.75) 的空 HashMap

 public HashMap(int initialCapacity) {}
 // 通過(guò)傳入的 Map 構(gòu)造一個(gè)使用默認(rèn)的加載因子的 HashMap
 public HashMap(Map<? extends K, ? extends V> m) {}

 // 構(gòu)造一個(gè)帶指定初始容量和加載因子的空 HashMap
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException();
        this.loadFactor = loadFactor;
        /* tableSizeFor() 把threshold 設(shè)置為最靠近且比 initialCapacity 大的2的冪次方,
         * 保證容量是2的冪次方
         * HashMap 中并沒(méi)有用于存儲(chǔ) initialCapacity 的變量
         * 而且散列表的數(shù)組 table 是懶加載的,所以在初始化前使用 threshold 存儲(chǔ) initialCapacity
         * 這種情況下在 table 初始化時(shí)會(huì)使用 threshold 作為 table 的容量
         */
        this.threshold = tableSizeFor(initialCapacity);
    }

三. 數(shù)據(jù)結(jié)構(gòu)

我們知道在 Java中 最常用的兩種結(jié)構(gòu)是數(shù)組和模擬指針(引用),幾乎所有的數(shù)據(jù)結(jié)構(gòu)都可以利用這兩種來(lái)組合實(shí)現(xiàn),HashMap 也是如此。HashMap 是一個(gè)“鏈表散列”,與 JDK 1.7 以及之前的版本不同,JDK1.8 在鏈表長(zhǎng)度大于8時(shí)會(huì)轉(zhuǎn)為用紅黑樹存儲(chǔ),當(dāng)長(zhǎng)度減少為6時(shí)又會(huì)轉(zhuǎn)為用鏈表存儲(chǔ)。如下是它數(shù)據(jù)結(jié)構(gòu):

hashMap內(nèi)存結(jié)構(gòu)圖

哈希桶數(shù)組和節(jié)點(diǎn) Node

哈希桶數(shù)組 Node<K,V>[] table使用一個(gè)用于存放鏈表或紅黑樹根節(jié)點(diǎn)的數(shù)組,使用哈希表進(jìn)行存儲(chǔ)。

transient Node<K,V>[] table;

Node 是 HashMap 的一個(gè)內(nèi)部類,實(shí)現(xiàn)了 Map.Entry 接口,本質(zhì)是就是一個(gè)映射(鍵值對(duì))。上圖中的每個(gè)黑色圓點(diǎn)就是一個(gè) Node 對(duì)象。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {...}
        public final K getKey()        {...}
        public final V getValue()      {...}
        public final String toString() {...}
        public final int hashCode()    {...}
        public final V setValue(V newValue)   {...}
        public final boolean equals(Object o) {...}
    }

哈希表存儲(chǔ)

HashMap 就是使用哈希表來(lái)存儲(chǔ)的。Java 中 HashMap 采用了鏈地址法。鏈地址法,簡(jiǎn)單來(lái)說(shuō),就是數(shù)組加鏈表的結(jié)合。在每個(gè)數(shù)組元素上都一個(gè)鏈表結(jié)構(gòu),當(dāng)數(shù)據(jù)被Hash后,得到數(shù)組下標(biāo),把數(shù)據(jù)放在對(duì)應(yīng)下標(biāo)元素的鏈表上。

在 HashMap 獲取 key 的 hashCode 時(shí),會(huì)調(diào)用 key 的 hashCode() 方法得到其hashCode,然后再通過(guò) hash()方法進(jìn)行二次哈希來(lái)定位該鍵值對(duì)的存儲(chǔ)位置,有時(shí)兩個(gè) key 會(huì)定位到相同的位置,表示發(fā)生了 Hash 碰撞。當(dāng)然 Hash 算法計(jì)算結(jié)果越分散均勻,Hash 碰撞的概率就越小,map的存取效率就會(huì)越高。

如果哈希桶數(shù)組很大,即使較差的Hash算法也會(huì)比較分散,如果哈希桶數(shù)組數(shù)組很小,即使好的Hash算法也會(huì)出現(xiàn)較多碰撞,所以就需要在空間成本和時(shí)間成本之間權(quán)衡,其實(shí)就是在根據(jù)實(shí)際情況確定哈希桶數(shù)組的大小,并在此基礎(chǔ)上設(shè)計(jì)好的hash算法減少Hash碰撞。那么通過(guò)什么方式來(lái)控制map使得Hash碰撞的概率又小,哈希桶數(shù)組(Node[] table)占用空間又少呢?答案就是好的 Hash 算法和擴(kuò)容機(jī)制。

在理解Hash和擴(kuò)容流程之前,我們得先了解下HashMap的幾個(gè)字段。從HashMap的默認(rèn)構(gòu)造函數(shù)源碼可知,構(gòu)造函數(shù)就是對(duì)下面幾個(gè)字段進(jìn)行初始化,源碼如下:

    transient int size;  // 當(dāng)前 HashMap 存儲(chǔ)的元素?cái)?shù)量
    transient int modCount;  // 修改計(jì)數(shù)器,用于 fail-fast 機(jī)制
    int threshold; // capacity * loadFactor,能容納的元素極限
    final float loadFactor; // 加載因子

由公式** threshold = capacity * loadFactor **可知,threshold就是在此loadFactor 和 capacity (數(shù)組長(zhǎng)度)對(duì)應(yīng)下允許存儲(chǔ)的最大元素?cái)?shù)目,超過(guò)這個(gè)數(shù)目就重新resize(擴(kuò)容),擴(kuò)容后的HashMap容量是之前容量的兩倍。默認(rèn)的負(fù)載因子0.75是對(duì)空間和時(shí)間效率的一個(gè)平衡選擇,顯然 loadFactor 越大碰撞就越不容易發(fā)生但會(huì)耗費(fèi)更多的空間;loadFactor 越小,耗費(fèi)的空間會(huì)減少但是碰撞會(huì)增多導(dǎo)致時(shí)間效率下降。如果內(nèi)存空間很多而又對(duì)時(shí)間效率要求很高,可以降低負(fù)載因子Load factor的值;相反,如果內(nèi)存空間緊張而對(duì)時(shí)間效率要求不高,可以增加負(fù)載因子loadFactor的值,這個(gè)值可以大于1。

在 HashMap 中,哈希桶數(shù)組table的長(zhǎng)度length大小必須為2的n次方,這是一種非常規(guī)的設(shè)計(jì),常規(guī)的設(shè)計(jì)是把桶的大小設(shè)計(jì)為素?cái)?shù)。相對(duì)來(lái)說(shuō)素?cái)?shù)導(dǎo)致沖突的概率要小于合數(shù),具體證明可以參考
http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable 初始化桶大小為11,就是桶大小設(shè)計(jì)為素?cái)?shù)的應(yīng)用(Hashtable 擴(kuò)容后不能保證還是素?cái)?shù))。HashMap 采用這種非常規(guī)設(shè)計(jì),主要是為了在取模和擴(kuò)容時(shí)做優(yōu)化,同時(shí)為了減少?zèng)_突,HashMap 定位哈希桶索引位置時(shí),也加入了高位參與運(yùn)算的過(guò)程。但這里存在一個(gè)問(wèn)題,即使負(fù)載因子和Hash算法設(shè)計(jì)的再合理,也免不了會(huì)出現(xiàn)拉鏈過(guò)長(zhǎng)的情況,一旦出現(xiàn)拉鏈過(guò)長(zhǎng),則會(huì)嚴(yán)重影響HashMap的性能。于是,在 JDK1.8 版本中,對(duì)數(shù)據(jù)結(jié)構(gòu)做了進(jìn)一步的優(yōu)化,引入了紅黑樹。而當(dāng)鏈表長(zhǎng)度太長(zhǎng)(默認(rèn)超過(guò)8)時(shí),鏈表就轉(zhuǎn)換為紅黑樹,利用紅黑樹快速增刪改查的特點(diǎn)提高 HashMap 的性能。

方法實(shí)現(xiàn)

HashMap的內(nèi)部功能實(shí)現(xiàn)很多,本文主要從根據(jù)key獲取哈希桶數(shù)組索引位置、put方法的詳細(xì)執(zhí)行、擴(kuò)容過(guò)程三個(gè)具有代表性的點(diǎn)深入展開講解

1. 二次哈希 - hash() 方法

前面說(shuō)過(guò)HashMap的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合,HashMap 通過(guò) hashCode 確定 key 在數(shù)組的位置。所以 HashMap 里面的元素位置分布得越均勻越好,盡量使得每個(gè)位置上的元素?cái)?shù)量只有一個(gè),那么當(dāng)我們根據(jù) hashCode 找到指定位置時(shí)就不用遍歷鏈表,大大優(yōu)化了查詢的效率。先看看源碼的實(shí)現(xiàn):

static final int hash(Object key) {
        int h;
        // h = key.hashCode()  取 hashCode 值
        // h ^ (h >>> 16)      將高位與低位進(jìn)行異或,
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在 JDK1.8 的實(shí)現(xiàn)中,優(yōu)化了高位運(yùn)算的算法,通過(guò) hashCode() 的高16位異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質(zhì)量來(lái)考慮的,這么做可以在數(shù)組 table 的 length 比較小的時(shí)候,也能保證考慮到高低位都參與到 HashCode 的計(jì)算中,同時(shí)不會(huì)有太大的開銷。

在 JDK1.8 中通過(guò) (n - 1) & hash 定位 key 在數(shù)組中位置,其中由于 table 的大小總是2的n次方,所以通過(guò)位與替代了取模(二者是等效的)大大提升了效率。
下面舉例說(shuō)明下,n為table的長(zhǎng)度。

hashMap哈希算法例圖

2. 存儲(chǔ)實(shí)現(xiàn) - put() 方法

下面是 put() 方法的流程圖,結(jié)合流程圖可以更好的理解源碼


Paste_Image.png
    // 與 JDK 1.7 不同,JDK 1.8 將存儲(chǔ)過(guò)程的實(shí)際處理邏輯放在 putVal() 方法中
    public V put(K key, V value) {
        // 對(duì) key 進(jìn)行二次 hash
        return putVal(hash(key), key, value, false, true);
    }

    // onlyIfAbsent 為 true, 不修改已經(jīng)存在的 value 
    // evict 為 false, 數(shù)組 table 處于 creation 模式
    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ù)組 table 是否為空或長(zhǎng)度為零,否則執(zhí)行resize()進(jìn)行擴(kuò)容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根據(jù) hash 值獲得對(duì)應(yīng)的數(shù)組索引 i,如果table[i]==null,則直接添加新節(jié)點(diǎn)
        // 此時(shí) p 為table[i] 的首元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        //此時(shí) table[i] 不為空,即發(fā)生了 hash 碰撞
        else {
            Node<K,V> e; K k;
            // 如果根節(jié)點(diǎn)的 key 和 傳入的 key 相同則在下一個(gè) if 語(yǔ)句修改 value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 判斷 table[i] 是否為 treeNode (紅黑樹),如果是紅黑樹則在樹中插入鍵值對(duì)
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 遍歷 table[i] 的鏈表
                for (int binCount = 0; ; ++binCount) {
                    // 要是下一個(gè)元素為空則插入一個(gè)新的節(jié)點(diǎn)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 要是鏈表長(zhǎng)度大于 8,則將鏈表轉(zhuǎn)為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) t
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 要是 key 已經(jīng)存在則下一個(gè) if 語(yǔ)句修改其 value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 在這里集中進(jìn)行 value 修改,要是需要修改 value 則 e 肯定不為空
            if (e != null) { 
                V oldValue = e.value;
                // onlyIfAbsent 為 true 則不修改已經(jīng)存在的 value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // HashMap 中為空,留給 LinkedHashMap 實(shí)現(xiàn)
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 要是當(dāng)前元素的數(shù)量超過(guò) threshold 則擴(kuò)容
        if (++size > threshold)
            resize();
        // HashMap 中為空,留給 LinkedHashMap 實(shí)現(xiàn)
        afterNodeInsertion(evict);
        return null;
    }

3. 擴(kuò)容實(shí)現(xiàn) - resize() 方法

HashMap 對(duì)數(shù)組 table 是懶加載的,只有當(dāng)?shù)谝淮握{(diào)用 put() 方法時(shí)才會(huì)調(diào)用 resize() 方法進(jìn)行初始化。resize() 方法的主要功能有兩個(gè):

  • 對(duì)數(shù)組 table 進(jìn)行初始化,默認(rèn)容量為16;
  • 對(duì)數(shù)字 table 進(jìn)行擴(kuò)容,保證其 capacity 是上一次的兩倍,而且始終為2的n次方。
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // 如果 table 沒(méi)有初始化則為零,否則為數(shù)組的長(zhǎng)度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;        
        /*
         * 擴(kuò)容或初始化前的準(zhǔn)備操作:設(shè)置 newCap 和 newThr
         * 如果 table 已經(jīng)初始化則進(jìn)行擴(kuò)容
         * 否則根據(jù)初始化使用的構(gòu)造函數(shù)進(jìn)行相應(yīng)的初始化準(zhǔn)備操作
         */
        // 初始化前 oldCap=0,oldCap>0 則表示已初始化
        if (oldCap > 0) {
            // oldCap > 0,此時(shí) table 已經(jīng)初始化,進(jìn)行擴(kuò)容操作
            // 如果 oldCap 大于 2^30 則把 threshold 設(shè)為最大,不再對(duì)數(shù)組擴(kuò)容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 容量擴(kuò)大兩倍后小于 MAXIMUM_CAPACITY 且 oldCap>16,則將threshold擴(kuò)大兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 要是條件不成立 newThr=0
        }
        // 此時(shí) oldCap=0 需要進(jìn)行初始化的準(zhǔn)備
        else if (oldThr > 0)
            // 此時(shí) oldThr 被設(shè)置(即 threshold),new HashMap 時(shí)使用下面的構(gòu)造函數(shù)
            // HashMap(int initialCapacity) 
            // HashMap(int initialCapacity, float loadFactor) 
            newCap = oldThr;
        else {               
            // threshold=0,對(duì)應(yīng)構(gòu)造方法 HashMap()          
            newCap = DEFAULT_INITIAL_CAPACITY; // 將 newCap 設(shè)置為默認(rèn)值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            // 對(duì)應(yīng)構(gòu)造函數(shù)
            // HashMap(int initialCapacity) 
            // HashMap(int initialCapacity, float loadFactor) 
            // 或者在下面條件不成立時(shí)重新計(jì)算 newThr 
            // (newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY
            float ft = (float)newCap * loadFactor;
            // 因?yàn)槭褂玫氖侵付ǖ?nitialCapacity,所以需要對(duì) newThr 進(jìn)行判斷處理
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
         // 根據(jù) newCap 創(chuàng)建新數(shù)組,和 設(shè)置新的 threshold 
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab; 

        /****** 初始化到此結(jié)束,下面是擴(kuò)容操作 *****/  
        if (oldTab != null) {
            // 將數(shù)據(jù)從舊數(shù)組遷移到新數(shù)組
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 如果首元素后面沒(méi)有元素則直接遷移到新數(shù)組
                    // 因?yàn)閿U(kuò)容后會(huì)與首元素發(fā)生 hash 碰撞的只能是其后面跟著的元素
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果是紅黑樹則轉(zhuǎn)到紅黑樹的處理方法
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
                        /*
                        * 處理鏈表,擴(kuò)容后 hash 不一定仍相同
                        * 假設(shè)容量從 16 增加到32
                        * 首元素和下一個(gè)元素 hash 前五位為 00001 和 10001
                        * 容量為16時(shí),hash&(cap-1) 均為 0001
                        * 而容量為32時(shí), hash&(cap-1) 則為 00001 和 10001
                        * 同時(shí)容量為16時(shí), hash&cap 則為 00000 和 10000 
                        * */
                        Node<K,V> loHead = null, loTail = null; // 低位 hash 
                        Node<K,V> hiHead = null, hiTail = null; // 高位 hash
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 擴(kuò)容時(shí)增加的那一位為零,hash 不變
                            // loHead 指向第一個(gè)元素,loTail 指向最后一個(gè)元素
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 擴(kuò)容時(shí)增加的那一位為一,hash = hash + oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 向新數(shù)組添加遷移數(shù)據(jù)
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

相關(guān)文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • HashMap 是 Java 面試必考的知識(shí)點(diǎn),面試官?gòu)倪@個(gè)小知識(shí)點(diǎn)就可以了解我們對(duì) Java 基礎(chǔ)的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,807評(píng)論 9 107
  • 學(xué)習(xí)資料; 《Java程序性能優(yōu)化》 美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì) Java 8系列之重新認(rèn)識(shí)HashMap 張旭童大佬 面試...
    英勇青銅5閱讀 2,929評(píng)論 3 97
  • 本文為jdk1.8源碼。跟1.7最大的區(qū)別是引入了紅黑樹。1.8之前HashMap的數(shù)據(jù)結(jié)構(gòu)為【數(shù)組-鏈表】 1...
    maven_hz閱讀 249評(píng)論 0 1
  • 我把自己的一些經(jīng)歷寫出來(lái),只因?yàn)槲沂侵钡浇裉觳琶靼自撊绾位ㄥX,而世面上并沒(méi)有人把這些道理講出來(lái),或者還有很多人沒(méi)有...
    風(fēng)流花吹雪閱讀 1,231評(píng)論 0 0
  • 這幾天和妻子經(jīng)常鬧別扭。我總嫌她把我管的過(guò)嚴(yán),覺得她對(duì)我不夠好,說(shuō)白了還是自己是在乎她的,在乎她的感受的。但是我與...
    63827048f0e2閱讀 412評(píng)論 0 0

友情鏈接更多精彩內(nèi)容