Java集合源碼分析(四)HashMap
一、HashMap簡介
1.1、HashMap概述
HashMap是基於哈希表的Map接口實現的,它存儲的是內容是鍵值對<key,value>映射。此類不保證映射的順序,假定哈希函數將元素適當的分布在各桶之間,可為基本操作(get和put)提供穩定的性能。
在API中給出了相應的定義:
//1、哈希表基於map接口的實現,這個實現提供了map所有的操作,並且提供了key和value可以為null,(HashMap和HashTable大致上是一樣的除了hashmap是異步的和允許key和value為null), 這個類不確定map中元素的位置,特別要提的是,這個類也不確定元素的位置隨著時間會不會保持不變。 Hash table based implementation of the MapHashMap的定義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.//假設哈希函數將元素合適的分到了每個桶(其實就是指的數組中位置上的鏈表)中,則這個實現為基本的操作(get、put)提供了穩定的性能,叠代這個集合視圖需要的時間跟hashMap實例(key-value映射的數量)的容量(在桶中) 成正比,因此,如果叠代的性能很重要的話,就不要將初始容量設置的太高或者loadfactor設置的太低,【這裏的桶,相當於在數組中每個位置上放一個桶裝元素】 This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the"capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings ). Thus, it‘s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important. //HashMap的實例有兩個參數影響性能,初始化容量(initialCapacity)和loadFactor加載因子,在哈希表中這個容量是桶的數量【也就是數組的長度】,一個初始化容量僅僅是在哈希表被創建時容量,在 容量自動增長之前加載因子是衡量哈希表被允許達到的多少的。當entry的數量在哈希表中超過了加載因子乘以當前的容量,那麽哈希表被修改(內部的數據結構會被重新建立)所以哈希表有大約兩倍的桶的數量 An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets. //通常來講,默認的加載因子(0.75)能夠在時間和空間上提供一個好的平衡,更高的值會減少空間上的開支但是會增加查詢花費的時間(體現在HashMap類中get、put方法上),當設置初始化容量時,應該考慮到map中會存放 entry的數量和加載因子,以便最少次數的進行rehash操作,如果初始容量大於最大條目數除以加載因子,則不會發生 rehash 操作。 As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur. //如果很多映射關系要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量創建它將使得映射關系能更有效地存儲。 If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table
1.2、HashMap在JDK1.8以前數據結構和存儲原理
1)鏈表散列
首先我們要知道什麽是鏈表散列?通過數組和鏈表結合在一起使用,就叫做鏈表散列。這其實就是hashmap存儲的原理圖。
2)HashMap的數據結構和存儲原理
HashMap的數據結構就是用的鏈表散列。那HashMap底層是怎麽樣使用這個數據結構進行數據存取的呢?分成兩個部分:
第一步:HashMap內部有一個entry的內部類,其中有四個屬性,我們要存儲一個值,則需要一個key和一個value,存到map中就會先將key和value保存在這個Entry類創建的對象中。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; //就是我們說的map的key V value; //value值,這兩個都不陌生 Entry<K,V> next;//指向下一個entry對象 int hash;//通過key算過來的你hashcode值。
Entry的物理模型圖:
第二步:構造好了entry對象,然後將該對象放入數組中,如何存放就是這hashMap的精華所在了。
大概的一個存放過程是:通過entry對象中的hash值來確定將該對象存放在數組中的哪個位置上,如果在這個位置上還有其他元素,則通過鏈表來存儲這個元素。
3)Hash存放元素的過程
通過key、value封裝成一個entry對象,然後通過key的值來計算該entry的hash值,通過entry的hash值和數組的長度length來計算出entry放在數組中的哪個位置上面,
每次存放都是將entry放在第一個位置。在這個過程中,就是通過hash值來確定將該對象存放在數組中的哪個位置上。
1.3、JDK1.8後HashMap的數據結構
上圖很形象的展示了HashMap的數據結構(數組+鏈表+紅黑樹),桶中的結構可能是鏈表,也可能是紅黑樹,紅黑樹的引入是為了提高效率。
1.4、HashMap的屬性
HashMap的實例有兩個參數影響其性能。
初始容量:哈希表中桶的數量
加載因子:哈希表在其容量自動增加之前可以達到多滿的一種尺度
當哈希表中條目數超出了當前容量*加載因子(其實就是HashMap的實際容量)時,則對該哈希表進行rehash操作,將哈希表擴充至兩倍的桶數。
Java中默認初始容量為16,加載因子為0.75。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 static final float DEFAULT_LOAD_FACTOR = 0.75f;
1)loadFactor加載因子
定義:loadFactor譯為裝載因子。裝載因子用來衡量HashMap滿的程度。loadFactor的默認值為0.75f。計算HashMap的實時裝載因子的方法為:size/capacity,而不是占用桶的數量去除以capacity。
loadFactor加載因子是控制數組存放數據的疏密程度,loadFactor越趨近於1,那麽數組中存放的數據(entry)也就越多,也就越密,也就是會讓鏈表的長度增加,loadFactor越小,也就是趨近於0,
那麽數組中存放的數據也就越稀,也就是可能數組中每個位置上就放一個元素。那有人說,就把loadFactor變為1最好嗎,存的數據很多,但是這樣會有一個問題,就是我們在通過key拿到我們的value時,
是先通過key的hashcode值,找到對應數組中的位置,如果該位置中有很多元素,則需要通過equals來依次比較鏈表中的元素,拿到我們的value值,這樣花費的性能就很高,
如果能讓數組上的每個位置盡量只有一個元素最好,我們就能直接得到value值了,所以有人又會說,那把loadFactor變得很小不就好了,但是如果變得太小,在數組中的位置就會太稀,也就是分散的太開,
浪費很多空間,這樣也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情況下不需要更改它。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
2)桶
根據前面畫的HashMap存儲的數據結構圖,你這樣想,數組中每一個位置上都放有一個桶,每個桶裏就是裝一個鏈表,鏈表中可以有很多個元素(entry),這就是桶的意思。也就相當於把元素都放在桶中。
3)capacity
capacity譯為容量代表的數組的容量,也就是數組的長度,同時也是HashMap中桶的個數。默認值是16。
一般第一次擴容時會擴容到64,之後好像是2倍。總之,容量都是2的冪。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
4)size的含義
size就是在該HashMap的實例中實際存儲的元素的個數
5)threshold的作用
threshold = capacity * loadFactor,當Size>=threshold的時候,那麽就要考慮對數組的擴增了,也就是說,這個的意思就是衡量數組是否需要擴增的一個標準。
註意這裏說的是考慮,因為實際上要擴增數組,除了這個size>=threshold條件外,還需要另外一個條件。
什麽時候會擴增數組的大小?在put一個元素時先size>=threshold並且還要在對應數組位置上有元素,這才能擴增數組。
int threshold;
我們通過一張HashMap的數據結構圖來分析:
二、HashMap的源碼分析(一)
2.1、HashMap的層次關系與繼承結構
1)HashMap繼承結構
上面就繼承了一個abstractMap,也就是用來減輕實現Map接口的編寫負擔。
2)實現接口
Map<K,V>:在AbstractMap抽象類中已經實現過的接口,這裏又實現,實際上是多余的。但每個集合都有這樣的錯誤,也沒過大影響
Cloneable:能夠使用Clone()方法,在HashMap中,實現的是淺層次拷貝,即對拷貝對象的改變會影響被拷貝的對象。
Serializable:能夠使之序列化,即可以將HashMap對象保存至本地,之後可以恢復狀態。
2.2、HashMap類的屬性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列號 private static final long serialVersionUID = 362498820763181265L; // 默認的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹 static final int TREEIFY_THRESHOLD = 8; // 當桶(bucket)上的結點數小於這個值時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中結構轉化為紅黑樹對應的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存儲元素的數組,總是2的冪次倍 transient Node<k,v>[] table; // 存放具體元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的個數,註意這個不等於數組的長度。 transient int size; // 每次擴容和更改map結構的計數器 transient int modCount; // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容 int threshold; // 填充因子 final float loadFactor; }屬性
2.3、HashMap的構造方法
有四個構造方法,構造方法的作用就是記錄一下16這個數給threshold(這個數值最終會當作第一次數組的長度。)和初始化加載因子。註意,hashMap中table數組一開始就已經是個沒有長度的數組了。
構造方法中,並沒有初始化數組的大小,數組在一開始就已經被創建了,構造方法只做兩件事情,一個是初始化加載因子,另一個是用threshold記錄下數組初始化的大小。註意是記錄。
1)HashMap()
//看上面的註釋就已經知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75 //初始化容量:也就是初始化數組的大小 //加載因子:數組上的存放數據疏密程度。 public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
2)HashMap(int)
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
3)HashMap(int,float)
public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能小於0,否則報錯 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 初始容量不能大於最大值,否則為最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 填充因子不能小於或等於0,不能為非數字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化填充因子 this.loadFactor = loadFactor; // 初始化threshold大小 this.threshold = tableSizeFor(initialCapacity); }
4)HashMap(Map<? extends K, ? extends V> m)
public HashMap(Map<? extends K, ? extends V> m) { // 初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR; // 將m中的所有元素添加至HashMap中 putMapEntries(m, false); }
putMapEntries(Map<? extends K, ? extends V> m, boolean evict)函數將m的所有元素存入本HashMap實例中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判斷table是否已經初始化 if (table == null) { // pre-size // 未初始化,s為m的實際元素個數 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 計算得到的t大於閾值,則初始化閾值 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,並且m元素個數大於閾值,進行擴容處理 else if (s > threshold) resize(); // 將m中的所有元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
三、HashMap源碼分析(二)
這裏我們來看一下我們常用的一些方法的源碼
3.1、put方法
1)put(K key,V value)
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
2)putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table未初始化或者長度為0,進行擴容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 確定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在數組中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已經存在元素 else { Node<K,V> e; K k; // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 將第一個元素賦值給e,用e來記錄 e = p; // hash值不相等,即key不相等;為紅黑樹結點 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; } // 判斷鏈表中結點的key值與插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環 break; // 用於遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表 p = e; } } // 表示在桶中找到key值、hash值與插入元素相等的結點 if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent為false或者舊值為null if (!onlyIfAbsent || oldValue == null) //用新值替換舊值 e.value = value; // 訪問後回調 afterNodeAccess(e); // 返回舊值 return oldValue; } } // 結構性修改 ++modCount; // 實際大小大於閾值則擴容 if (++size > threshold) resize(); // 插入後回調 afterNodeInsertion(evict); return null; }
HashMap並沒有直接提供putVal接口給用戶調用,而是提供的put函數,而put函數就是通過putVal來插入元素的。
3)putAlll()
3.2、get方法
1)get(Object key)
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
2)getNode(int hash,Pbject key)
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // table已經初始化,長度大於0,根據hash尋找table中的項也不為空 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) { // 為紅黑樹結點 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; }
HashMap並沒有直接提供getNode接口給用戶調用,而是提供的get函數,而get函數就是通過getNode來取得元素的。
3.3、resize方法
final Node<K,V>[] resize() { // 當前table保存 Node<K,V>[] oldTab = table; // 保存table大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 保存當前閾值 int oldThr = threshold; int newCap, newThr = 0; // 之前table大小大於0 if (oldCap > 0) { // 之前table大於最大容量 if (oldCap >= MAXIMUM_CAPACITY) { // 閾值為最大整形 threshold = Integer.MAX_VALUE; return oldTab; } // 容量翻倍,使用左移,效率更高 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 閾值翻倍 newThr = oldThr << 1; // double threshold } // 之前閾值大於0 else if (oldThr > 0) newCap = oldThr; // oldCap = 0並且oldThr = 0,使用缺省值(如使用HashMap()構造函數,之後再插入一個元素會調用resize函數,會進入這一步) else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新閾值為0 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"}) // 初始化table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 之前的table已經初始化過 if (oldTab != null) { // 復制元素,重新進行hash 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; // 將同一桶中的元素根據(e.hash & oldCap)是否為0進行分割,分成兩個不同的鏈表,完成rehash do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } 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; }resize()
進行擴容,會伴隨著一次重新hash分配,並且會遍歷hash表中所有的元素,是非常耗時的。在編寫程序中,要盡量避免resize。
在resize前和resize後的元素布局如下:
上圖只是針對了數組下標為2的桶中的各個元素在擴容後的分配布局,其他各個桶中的元素布局可以以此類推。
四、總結
4.1、關於數組擴容
從putVal源代碼中我們可以知道,當插入一個元素的時候size就加1,若size大於threshold的時候,就會進行擴容。假設我們的capacity大小為32,loadFator為0.75,則threshold為24 = 32 * 0.75,
此時,插入了25個元素,並且插入的這25個元素都在同一個桶中,桶中的數據結構為紅黑樹,則還有31個桶是空的,也會進行擴容處理,其實,此時,還有31個桶是空的,好像似乎不需要進行擴容處理,
但是是需要擴容處理的,因為此時我們的capacity大小可能不適當。我們前面知道,擴容處理會遍歷所有的元素,時間復雜度很高;前面我們還知道,經過一次擴容處理後,元素會更加均勻的分布在各個桶中,
會提升訪問效率。所以,說盡量避免進行擴容處理,也就意味著,遍歷元素所帶來的壞處大於元素在桶中均勻分布所帶來的好處。
4.2、總結
1)要知道hashMap在JDK1.8以前是一個鏈表散列這樣一個數據結構,而在JDK1.8以後是一個數組加鏈表加紅黑樹的數據結構。
2)通過源碼的學習,hashMap是一個能快速通過key獲取到value值得一個集合,原因是內部使用的是hash查找值得方法。
參考博文:
http://www.cnblogs.com/whgk/p/6091316.html
http://www.cnblogs.com/leesf456/p/5242233.html
Java集合源碼分析(四)HashMap