1. 程式人生 > >Java HashMap實現原理2——HashMap詳解

Java HashMap實現原理2——HashMap詳解

博主的前兩篇文章Java HashMap實現原理0——從hashCode,equals說起Java HashMap實現原理1——散列表已經講述了HashMap設計的知識點,包括:hashCode(),equals(),散列表結構,雜湊函式、衝突解決等,在散列表一文最後,還給出了一個極簡版本的實現。從極簡版出發,理解java.util.HashMap,就容易多了。
最近博主偶遇了幾家公司的技術文章,有一種“相見恨晚“的感覺,繼上次和大家分享騰訊Bugly之後,這裡再和大家分享美團點評的技術網址。也歡迎大家評論裡貢獻自己知道的一些高質量的技術網址,多了之後博主專門整理出來,大家一起學習進步。

Map

鍵值對的儲存在實際程式設計中使用廣泛,Java中實現了HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap等類。它們之間的關係如下圖所示:
這裡寫圖片描述
現代版的*Map均從Map介面下來,已經不推薦使用的HashTable繼承自Dictionary。

  1. HashMap根據物件的HashCode存讀資料,多數情況下可以在O(1)的時間內訪問,HashMap允許有null鍵值對,但是null鍵只能有一個。HashMap是執行緒不安全的,如果要在多執行緒情況下使用,可以使用Collections的synchronizedMap方法,或者直接使用ConcurrentHashMap。
  2. LinkedHashMap是HashMap的子類,記錄了元素放入的順序,使用Iterator遍歷的時候,會按放入的順序依次讀出元素。
  3. TreeMap會預設按鍵排序,使用Iterator獲取時,會按排好的順序返回。需要注意的是,鍵必須實現了Comparable介面,或者在構造TreeMap的時候傳入自定義的Compartor類。
  4. HashTable,老版本jdk遺留下來的類,執行緒安全,功能上和HashMap幾乎一樣,併發效能不如引入了分段鎖的ConcurrentHashMap。

以上的Map型別類,要求Key都是物件不可變的,即物件建立後hash值不可變。可採用的方式包括:以String、Integer等型別作為鍵,或者使用物件中不變屬性建立hash值。

HashMap

Map中最常見的是HashMap,它的實現結構就是我們上文提到的散列表(陣列+連結串列)方式,採用鏈地址法處理衝突,連結串列的缺點在於,當衝突很多的時候,連結串列的查詢速度為O(n),JDK8對HashMap的散列表進行了改進,當連結串列儲存元素大於8個時,由紅黑樹結構代替連結串列結構,這樣查詢速度就降到了O(logn)。
先看一個HashMap常見的使用:

HashMap<String,Integer> map = new HashMap();
map.put("a", 1);
map.put("b", 2);
System.out.println(map.get("a"));

先建立HashMap物件,再利用put放入元素,get方法獲取。有了上一篇散列表中對一個小demo的介紹,相信大家對HashMap的結構應該還有大致的印象,忘記的可以在Java HashMap實現原理1——散列表文章最後部分檢視。

1.Node

先看HashMap中定義的Node

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用來定位陣列索引位置
        final K key;
        V value;
        Node<K,V> next;   //連結串列的下一個node
        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) { ... }
}

對比demo中的Node,除了基本的key、 value、next之外,還提供了hash屬性,以及屬性存取的操作介面等,複寫了hashCode()和equals()方法。可見一個可用、完善的類庫這些都是必備條件。結合使用例項,我們先看下HashMap的建構函式。

2.HashMap構造器

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

構造器中初始化了loadFactor、threshold等變數,loadFactor表示負載因子、threshold表示HashMap允許容納的元素個數,threshold=Array.size()*loadFactor,也就是說在當前陣列大小以及負載因子的限定下,HashMap最多能裝的資料個數,當超過這個值時,就得陣列進行擴容resize。系統預設的loadFactor=0.75,值越大,查詢效率降低,空間利用率提高;值越小,查詢效率增大,空間利用率降低,所以一般不建議修改。還有個變數size要與threshold和loadFactor區別開,它表示當前裝載的元素個數。很奇怪,建構函式中沒有初始化陣列,直覺中一般在構造器中完成空間分配的操作,接著往下看。

3.HashMap的put()

put是HashMap的精華部分,裡面用到了hash()、resize()、transfer()等函式,涉及到到不少數學計算中的技巧,我們慢慢欣賞。
put()方法如下:

/*put方法*/
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
/*putVal方法*/
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為空則建立,put的時候才建立空間,相當於延遲初始化,減少不必要的建立,比在new HashMap()中建立要高明。
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     // ②:根據hash值計算index
     if ((p = tab[i = (n - 1) & hash]) == null) 
         //對null做處理 
         tab[i] = newNode(hash, key, value, null);
     else {
         Node<K,V> e; 
         K k;
         //③:節點key存在,直接覆蓋value
         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);
                     //連結串列長度大於8轉換為紅黑樹進行處理
                     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;
             }
         }

         if (e != null) { // existing mapping for key
             V oldValue = e.value;
             if (!onlyIfAbsent || oldValue == null)
                 e.value = value;
             afterNodeAccess(e);
             return oldValue;
         }
     }
     ++modCount;//主要用來記錄HashMap內部結構發生變化的次數,用於迭代的快速失敗(內部結構發生變化指的是結構發生變化,例如put新鍵值對,但是某個key對應的value值被覆蓋不屬於結構變化)
     //⑥:超過最大容量 就擴容
     if (++size > threshold)
         resize();
     afterNodeInsertion(evict);
     return null;
 }

put方法中,先呼叫hash()計算key。我們看下hash的實現

static final int hash(Object key) {   
     int h;
     // 第一步:h = key.hashCode()
     // 第二步:h ^ (h >>> 16) 高16位和低16位進行異或運算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

有了hash值之後,會根據hash值計算出元素在陣列中的存放位置(n - 1) & hash,乍一看可能會有人有疑問,不是%取模嗎?為什麼用了個按位與。看了下面這張圖片,你可能就明白了:
這裡寫圖片描述
從生成hashCode開始,到hash()對hashCode進行高16位和低16位的異或操作得到hash值,再利用hash值換算存放的陣列位置。按位與實現了只保留低log(n)的效果,而這log(n)位的取值範圍為[0,n-1],正是我們%取餘的效果,但是對CPU來說&比%省時。
putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)函式的流程如下圖所示,程式碼的關鍵位置也有註釋。關於紅黑樹,博主將在下篇文章中專門闡述,此數不再細說。
這裡寫圖片描述

4.HashMap的resize()

雖然resize是隻是put()的一個呼叫函式,但是resize()很精妙,寫慣了業務邏輯相關的程式碼中後,再看這種util程式碼,很是親切,我們一起來學習一番。
JDK8中的resize()稍微複雜點,我們先看個JDK7的resize()方法壓壓驚。

  • JDK7的resize()
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果容量已經達到最大值
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;//閾值放大為int的最大整數
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));//將元素全都放入到新的陣列中
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {//從原陣列中挨個取出連結串列
        while(null != e) {//遍歷連結串列的每個元素
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);//重新計算當前元素在新陣列中的位置
            e.next = newTable[i];//插入到新陣列對應連結串列的頭部
            newTable[i] = e;
            e = next;
        }
    }
}

流程在程式碼中註釋的比較詳細,就不多說了。

  • JDK8中的resize()方法
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;//閾值改為最大int整數
            return oldTab;
        }
        // 沒超過最大值,就擴充為原來的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; //左移1一位,擴大兩倍
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {
        // 陣列第一次被建立
        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 { 
                // 連結串列優化重hash的程式碼塊
                    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) {//尼瑪,這一塊博主理解了很久。。。oldCap對應於原來的陣列長度,陣列長度總是2的n次方,所以&==0表示倒數第n位(從0開始算)為0 ,意味著擴容到2n之後,該數不需要放入到新增的位置區域,保持原來的陣列位置不變。
                            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;
}

對比JDK7和JDK8的resize方法實現,除了JDK7將陣列轉移拆分到單獨的函式transfer裡面之外,JDK7在向新散列表插入元素時,每次都會將新到的元素插入連結串列頭,而JDK8會保持元素在舊陣列的相對位置,即連結串列從前往後依次插入。當然JDK8多了紅黑樹的使用,這個就不多說了。還有個博主糾結了很久的問題,在註釋中已經提過了,就是如何快速找到元素在新散列表中的位置,JDK7採用的是對hash值重新取餘,而JDK8中做了優化,先看圖:
這裡寫圖片描述
原大小為16的散列表中,位置為5的元素,在新的32大小的散列表中位置為5+16,取餘也可以計算得到,但是JDK8用了更快的方法,e.hash & oldCap==0(oldCap是原散列表中陣列的大小),則表示元素的位置在新的散列表中為j + oldCap,否則不變。
這裡寫圖片描述
對於hash1和hash2這樣兩個hash值的key,在16大小的散列表中,它們對1111(二進位制)取餘,都得到5,在32大小的散列表中,他們對11111(二進位制)取餘,前者為5,後者為5+16=21。想必看著圖,大家就明白為什麼這麼coding了。
這樣,精華的put部分就講完了,可以看到,巧用位操作,會給你帶來效能上的提升。

5.HashMap的get()

再看下get方法的實現,這個就比較簡單了,程式碼如下:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}
/*getEntry()*/
final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

現由key計算得到hash值,再換算到陣列中的位置,取出對應的連結串列,挨個檢查是否和待查詢key,hash值相同且是同一個類的物件或者同一個物件。

總結

本文對HashMap的Node結構、建構函式、put()方法、resize()過程(JDK7和JDK8分別做了分析)、get()方法進行了詳盡的分析,希望可以讓大家對HashMap加深理解。
本文感謝Java 8系列之重新認識HashMap
很慚愧,做了一點微小的貢獻!

相關推薦

Java HashMap實現原理2——HashMap

博主的前兩篇文章Java HashMap實現原理0——從hashCode,equals說起,Java HashMap實現原理1——散列表已經講述了HashMap設計的知識點,包括:hashCode(),equals(),散列表結構,雜湊函式、衝突解決等,在散列表

hashmap實現原理2

ace 數據 取數 tool 數組存儲 同時 個數 array jsb put和get都首先會調用hashcode方法,去查找相關的key,當有沖突時,再調用equals(這也是為什麽剛開始就重溫hashcode和equals的原因)! HashMap基於hashing原

zabbix實現原理及架構

收集 信息 核心 狀態 start 原理 整體架構 比較 zabbix 想要用好zabbix進行監控,那麽我們首要需要了解下zabbix這個軟件的實現原理及它的架構。建議多閱讀官方文檔。 一、總體上zabbix的整體架構如下圖所示: 重要組件說明: 1)zabbix se

HashMap實現原理

HashMap定義 HashMap實現了Map介面,一種將鍵對映到值得物件。 一個對映不能包含重複的鍵;每個鍵只能對映到一個值上。 HashMap的元素是無序的。要實現有序排列必須實現hashc

1.Java集合-HashMap實現原理及源碼分析

int -1 詳細 鏈接 理解 dac hash函數 順序存儲結構 對象儲存   哈希表(Hash Table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實

JavaHashMap原始碼分析——常用方法

上一篇介紹了HashMap的基本概念,這一篇著重介紹HasHMap中的一些常用方法:put()get()**resize()** 首先介紹resize()這個方法,在我看來這是HashMap中一個非常重要的方法,是用來調整HashMap中table的容量的,在很多操作中多需要重新計算容量。原始碼如下: 1

揭祕 HashMap 實現原理Java 8)

HashMap 作為一種容器型別,無論你是否瞭解過其內部的實現原理,它的大名已經頻頻出現在各種網際網路面試中了。從基本的使用角度來說,它很簡單,但從其內部的實現來看(尤其是 Java 8 的改進以來),它又並非想象中那麼容易。如果你一定要問了解其內部實現與否對於寫程式究竟有多大影響,我不能給出一個確切的答案。

java jdk7 hashMap實現原理

在官方文件中是這樣描述HashMap的: Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and

java HASHMAP 實現原理

1. HashMap概述:   HashMap是基於雜湊表的Map介面的非同步實現(Hashtable跟HashMap很像,唯一的區別是Hashtalbe中的方法是執行緒安全的,也就是同步的)。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對

JAVA複習資料-HashMap實現原理

1. HashMap的資料結構 資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。       陣列 陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難; 連結串列

HashMap實現原理

一個 ash img 方法 shm 步長 初始 2的n次冪 http HashMap的數據結構是數組+單向鏈表,數組裏面存儲就是鏈表的Head節點,鏈表節點存儲的是我們put進去的key/value。 如果要實現HashMap,主要有三個重要的功能點: 1.初

探索HashMap實現原理及其在jdk8數據結構的改進

重點 his 說了 比較 filter new exist adf 網絡 因為網上已經太多的關於HashMap的相關文章了,為了避免大量重復,又由於網上關於java8的HashMap的相關文章比較少,至少我沒有找到比較詳細的。所以才有了本文。 本文主要的內容: 1.Ha

HashMap實現原理及源碼分析

響應 應用場景 取模運算 圖片 mat 直接 maximum 計算 時間復雜度 哈希表(hash table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實現原理也

HashMap實現原理和源碼分析

aci 鍵值對 creat 變化 遍歷數組 沖突的解決 查看 seed 二分 作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>原文:https://www.cnblogs.com/cheng

轉:HashMap實現原理分析(面試問題:兩個hashcode相同 的對象怎麽存入hashmap的)

影響 strong 就會 怎麽 ash 地方 shm nbsp 擔心 原文地址:https://www.cnblogs.com/faunjoe88/p/7992319.html 主要內容: 1)put 疑問:如果兩個key通過hash%Entry[].length得到的

batchnorm原理及程式碼(筆記2

Batchnorm原理詳解 前言:Batchnorm是深度網路中經常用到的加速神經網路訓練,加速收斂速度及穩定性的演算法,可以說是目前深度網路必不可少的一部分。 本文旨在用通俗易懂的語言,對深度學習的常用演算法–batchnorm的原理及其程式碼實現做一個詳細的解讀。本文主要包括以下幾個

Java程式設計師從笨鳥到菜鳥之(七十二)細談Spring(四)利用註解實現spring基本配置

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

HashMap實現原理(部分原始碼)

JAVA裡面有HashMap、Hashtable、HashSet三種hash集合的實現原始碼,這裡總結下,理解錯誤的地方還望指正! HashMap和Hashtable的區別 1、兩者最主要的區別在於Hashtable是執行緒安全,而HashMap則非執行緒安全。 Hashtabl

HashMap實現原理及原始碼分析(轉載)

作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>        雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,

HashMap實現原理分析及簡單實現一個HashMap

HashMap實現原理分析及簡單實現一個HashMap 歡迎關注作者部落格 簡書傳送門 轉載@原文地址   HashMap的工作原理是近年來常見的Java面試題。幾乎每個Java程式設計師都知道HashMap,都知道哪裡要用HashMap,知道HashMap和