1. 程式人生 > >Java HashMap工作原理及實現

Java HashMap工作原理及實現

1. 概述

從本文你可以學習到:

  1. 什麼時候會使用HashMap?他有什麼特點?
  2. 你知道HashMap的工作原理嗎?
  3. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?
  4. 你知道hash的實現嗎?為什麼要這樣實現?
  5. 如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

當我們執行下面的操作時:

1
2
3
4
5
6
7
8
9
10
11
12
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("語文", 1);
map.put("數學"
, 2);
map.put("英語", 3); map.put("歷史", 4); map.put("政治", 5); map.put("地理", 6); map.put("生物", 7); map.put("化學", 8); for(Entry<String, Integer> entry : lmap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }

執行結果是

政治: 5
生物: 7
歷史: 4
數學: 2
化學: 8
語文: 1
英語: 3
地理: 6

發生了什麼呢?下面是一個大致的結構,希望我們對HashMap的結構有一個感性的認識:

在官方文件中是這樣描述HashMap的:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

幾個關鍵的資訊:基於Map介面實現、允許null鍵/值、非同步、不保證有序(比如插入的順序)、也不保證序不隨時間變化。

2. 兩個重要的引數

在HashMap中有兩個很重要的引數,容量(Capacity)和負載因子(Load factor)

  • Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
  • Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

簡單的說,Capacity就是bucket的大小,Load factor就是bucket填滿程度的最大比例。如果對迭代效能要求很高的話不要把capacity設定過大,也不要把load factor設定過小。當bucket中的entries的數目大於capacity*load factor時就需要調整bucket的大小為當前的2倍。

3. put函式的實現

put函式大致的思路為:

  1. 對key的hashCode()做hash,然後再計算index;
  2. 如果沒碰撞直接放到bucket裡;
  3. 如果碰撞了,以連結串列的形式存在buckets後;
  4. 如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹;
  5. 如果節點已經存在就替換old value(保證key的唯一性)
  6. 如果bucket滿了(超過load factor*current capacity),就要resize。

具體程式碼的實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public V put(K key, V value) {
    // 對key的hashCode()做hash
    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;
    // tab為空則建立
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 計算index,並對null做處理
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 節點存在
        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) {
                    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;
            }
        }
        // 寫入
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 超過load factor*current capacity,resize
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

4. get函式的實現

在理解了put之後,get就很簡單了。大致思路如下:

  1. bucket裡的第一個節點,直接命中;
  2. 如果有衝突,則通過key.equals(k)去查詢對應的entry
    若為樹,則在樹中通過key.equals(k)查詢,O(logn);
    若為連結串列,則在連結串列中通過key.equals(k)查詢,O(n)。

具體程式碼的實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 直接命中
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 未命中
        if ((e = first.next) != null) {
            // 在樹中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在連結串列中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

5. hash函式的實現

在get和put的過程中,計算下標時,先對hashCode進行hash操作,然後再通過hash值進一步計算下標,如下圖所示:

在對hashCode()計算hash時具體實現是這樣的:

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

可以看到這個函式大概的作用就是:高16bit不變,低16bit和高16bit做了一個異或。其中程式碼註釋是這樣寫的:

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

在設計hash函式時,因為目前的table長度n為2的冪,而計算下標的時候,是這樣實現的(使用&位操作,而非%求餘):

(n - 1) & hash

設計者認為這方法很容易發生碰撞。為什麼這麼說呢?不妨思考一下,在n - 1為15(0x1111)時,其實雜湊真正生效的只是低4bit的有效位,當然容易碰撞了。

因此,設計者想了一個顧全大局的方法(綜合考慮了速度、作用、質量),就是把高16bit和低16bit異或了一下。設計者還解釋到因為現在大多數的hashCode的分佈已經很不錯了,就算是發生了碰撞也用O(logn)的tree去做了。僅僅異或一下,既減少了系統的開銷,也不會造成的因為高位沒有參與下標的計算(table長度比較小時),從而引起的碰撞。

如果還是產生了頻繁的碰撞,會發生什麼問題呢?作者註釋說,他們使用樹來處理頻繁的碰撞(we use trees to handle large sets of collisions in bins),在JEP-180中,描述了這個問題:

Improve the performance of java.util.HashMap under high hash-collision conditions by using balanced trees rather than linked lists to store map entries. Implement the same improvement in the LinkedHashMap class.

之前已經提過,在獲取HashMap的元素時,基本分兩步:

  1. 首先根據hashCode()做hash,然後確定bucket的index;
  2. 如果bucket的節點的key不是我們需要的,則通過keys.equals()在鏈中找。

在Java 8之前的實現中是用連結串列解決衝突的,在產生碰撞的情況下,進行get時,兩步的時間複雜度是O(1)+O(n)。因此,當碰撞很厲害的時候n很大,O(n)的速度顯然是影響速度的。

因此在Java 8中,利用紅黑樹替換連結串列,這樣複雜度就變成了O(1)+O(logn)了,這樣在n很大的時候,能夠比較理想的解決這個問題,在Java 8:HashMap的效能提升一文中有效能測試的結果。

6. resize的實現

當put時,如果發現目前的bucket佔用程度已經超過了Load Factor所希望的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充為2倍,之後重新計算index,把節點再放到新的bucket中。resize的註釋是這樣描述的:

Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.

大致意思就是說,當超過限制的時候會resize,然而又因為我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。

怎麼理解呢?例如我們從16擴充套件為32時,具體的變化如下所示:

因此元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。可以看看下圖為16擴充為32的resize示意圖:

這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的衝突的節點分散到新的bucket了。

下面是程式碼的具體實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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;
        }
        // 沒超過最大值,就擴充為原來的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            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);
    }
    // 計算新的resize上限
    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) {
        // 把每個bucket都移動到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    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) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket裡
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket裡
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

7. 總結

我們現在可以回答開始的幾個問題,加深對HashMap的理解:

1. 什麼時候會使用HashMap?他有什麼特點?
是基於Map介面的實現,儲存鍵值對時,它可以接收null的鍵值,是非同步的,HashMap儲存著Entry(hash, key, value, next)物件。

2. 你知道HashMap的工作原理嗎?
通過hash的方法,通過put和get儲存和獲取物件。儲存物件時,我們將K/V傳給put方法時,它呼叫hashCode計算hash從而得到bucket位置,進一步儲存,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize為原來的2倍)。獲取物件時,我們將K傳給get,它呼叫hashCode計算hash從而得到bucket位置,並進一步呼叫equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過連結串列將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(預設是8),則使用紅黑樹來替換連結串列,從而提高速度。

3. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?
通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去連結串列或樹中去查詢對應的節點

4. 你知道hash的實現嗎?為什麼要這樣實現?
在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。

5. 如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
如果超過了負載因子(預設0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新呼叫hash方法。

以Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模桶陣列的大小可得到陣列下標。

插入元素時,如果兩條Key落在同一個桶(比如雜湊值1和17取模16後都屬於第一個雜湊桶),Entry用一個next屬性實現多個Entry以單向連結串列存放,後入桶的Entry將next指向桶當前的Entry。

查詢雜湊值為17的key時,先定位到第一個雜湊桶,然後以連結串列遍歷桶裡所有元素,逐個比較其key值。

當Entry數量達到桶數量的75%時(很多文章說使用的桶數量達到了75%,但看程式碼不是),會成倍擴容桶陣列,並重新分配所有原來的Entry,所以這裡也最好有個預估值。

取模用位運算(hash & (arrayLength-1))會比較快,所以陣列的大小永遠是2的N次方, 你隨便給一個初始值比如17會轉為32。預設第一次放入元素時的初始值是16。

iterator()時順著雜湊桶陣列來遍歷,看起來是個亂序。

在JDK8裡,新增預設為8的閥值,當一個桶裡的Entry超過閥值,就不以單向連結串列而以紅黑樹來存放以加快Key的查詢速度。

參考資料

相關推薦

Java HashMap工作原理實現

1. 概述 從本文你可以學習到: 什麼時候會使用HashMap?他有什麼特點?你知道HashMap的工作原理嗎?你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?你知道hash的實現嗎?為什麼要這樣實現?如果HashMap的大小超過了負

Java HashMap工作原理實現(二)

類宣告 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 功能和特點 實現Abs

Java】Java8 HashMap工作原理實現

1 、概述 從本文你可以學到 什麼時候會使用HashMap?他有什麼特點? 你知道HashMap的工作原理嗎? 你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用? 你知道hash的實現嗎?

Java LinkedList工作原理實現

++ 部分 更多 size mov info 容量限制 element git 1. 概述 以雙向鏈表實現。鏈表無容量限制,但雙向鏈表本身使用了更多空間,也需要額外的鏈表指針操作。 按下標訪問元素—get(i)/set(i,e) 要悲劇的遍歷鏈表將指針移動到位(如果i&g

Java 集合:TreeMap工作原理實現

前言 本文轉載自:點這裡,該部落格非常不錯,建議前去看看。 正文 1. 概述 A Red-Black tree based NavigableMap implementation. The map is sorted according to the natura

LVS-NAT和LVS-DR類型的工作原理實現步驟

本地 頭文件 交換機 響應 實現原理 arp 處理 直接 通告 lvs-dr類型工作原理:①:client端向目標IP(VIP)發送請求,經由路由器和交換機設備和後,此時的請求數據包頭文件的目標MAC值為調度器的MAC,源MAC值為client的MAC,目標IP為VIP,源

hibernate工作原理作用 JAVA Hibernate工作原理為什麼要用

轉載自 http://www.cnblogs.com/dashi/p/3597969.html#commentform JAVA Hibernate工作原理及為什麼要用 hibernate 簡介:hibernate是一個開源框架,它是物件關聯關係對映的框架,它對JDBC做了輕量級的封裝,而我們j

JAVA NIO工作原理程式碼示例

簡介:本文主要介紹了JAVA NIO中的Buffer, Channel, Selector的工作原理以及使用它們的若干注意事項,最後是利用它們實現伺服器和客戶端通訊的程式碼例項。 歡迎探討,如有錯誤敬請指正 1. ByteBuffer 1.1直接緩衝區和非直接緩衝區 下面是建立ByteBuffer物件的

Java動態代理原理實現

     最近專案和看技術文章的時候接觸了點Java動態代理和cglib、asm等知識,發現對於動態代理整套機制理解不夠,總以為是採取切片等方式,執行時利用反射,通過標記等在需代理方法或者介面等上下文中執行某種增強方法,未想到會有中間位元組碼的動態生成,看到博主的這篇文章,

5.CND技術詳解---全域性負載均衡工作原理實現

2.基於 DNS 解析的 GSLB 實現機制2.1 DNS工作流程:2.2 DNS 記錄型別及報文格式2.3 基於 DNS 解析的 GSLB 工作方式2.4 負載均衡的策略判斷條件3.基於 DNS 的 GSLB 應用部署方式4.基於應用層協議重定向的 GSLB5.基於 IP

JAVA Hibernate工作原理為什麼要用

hibernate 簡介:hibernate是一個開源框架,它是物件關聯關係對映的框架,它對JDBC做了輕量級的封裝,而我們java程式設計師可以使用面向物件的思想來操縱資料庫。hibernate核心介面session:負責被持久化物件CRUD操作sessionFactory:負責初始化hibernate,建

Java HashMap工作原理實現

目錄 概述 HashMap的基本操作如下: map.put("Chinese", 1); map.put("Math", 2); map.put("Englist", 3); map.put("Chemistry", 4); map.pu

Java HashMap工作原理 各種Map區別

一、Java HashMap的工作原理 面試的時候經常會遇見諸如:“java中的HashMap是怎麼工作的”,“HashMap的get和put內部的工作原理”這樣的問題。 Put : 讓我們看下put方法的實現: public V put(K key, V value) {

java設計模式singleton原理實現

最新 不必要 -- 不同 適合 所有 引用 ati cnblogs 題外話:我要變強,要變強,變強,強。 1、 Singleton的應用場景以及為什麽要使用singleSingleton是一生只能有一個實例的對象。只能由singleton自身創建一個實例。外人是無法創建實例

HTTP協議報文、工作原理Java中的HTTP通信技術詳解

tor 報文 buffered mod protoc 禁止 ans 請求報文 客戶端 博客園 首頁 新隨筆 聯系 管理 訂閱

【轉】Java學習---快速掌握RPC原理實現

消費者 阿裏 局限 kryo nes 很多 cal 網絡 href 【原文】https://www.toutiao.com/i6592365493435236872/ ?RPC概述 RPC(Remote Procedure Call)即遠程過程調用,也就是說兩臺服務器A,

深入理解Java中的底層阻塞原理實現

更多 安全 posix pla static events time() 方便 原理 談到阻塞,相信大家都不會陌生了。阻塞的應用場景真的多得不要不要的,比如 生產-消費模式,限流統計等等。什麽 ArrayBlockingQueue、 LinkedBlockingQueue、

Java集合-ConcurrentHashMap工作原理實現JDK7

概述 本文學習知識點 1.ConcurrentHashMap與HashMap的區別。 2.資料儲存結構。 3.如何提高併發讀寫效能。 4.put和get方法原始碼實現分析。 5.size方法如何實現。 與HashMap的區別 1.ConcurrentHashMap和Ha

Java集合-ConcurrentHashMap工作原理實現JDK8

概述 本文主要介紹ConcurrentHashMap在JDK8中的原始碼實現和原理。在JDK8中,開發人員幾乎把ConcurrentHashMap的原始碼重寫了一遍,原始碼由之前的2000多行增加到了6300行左右,因此實現也就複雜很多。在學習之前,最好先了解下如下知識: 1、Ree

HTTP協議報文、工作原理Java中的HTTP通訊技術詳解

一、web及網路基礎       1、HTTP的歷史            1.1、HTTP的概念:         &nb