HashMap是基於哈希表的Map接口實現的,它存儲的是內容是鍵值對<key,value>映射。此類不保證映射的順序,假定哈希函數將元素適當的分佈在各桶之間,可爲基本操做(get和put)提供穩定的性能。html
在API中給出了相應的定義:node
//一、哈希表基於map接口的實現,這個實現提供了map全部的操做,而且提供了key和value能夠爲null,(HashMap和HashTable大體上是同樣的除了hashmap是異步的和容許key和value爲null), 這個類不肯定map中元素的位置,特別要提的是,這個類也不肯定元素的位置隨着時間會不會保持不變。 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. //假設哈希函數將元素合適的分到了每一個桶(其實就是指的數組中位置上的鏈表)中,則這個實現爲基本的操做(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)鏈表散列數組
首先咱們要知道什麼是鏈表散列?經過數組和鏈表結合在一塊兒使用,就叫作鏈表散列。這其實就是hashmap存儲的原理圖。數據結構
2)HashMap的數據結構和存儲原理app
HashMap的數據結構就是用的鏈表散列。那HashMap底層是怎麼樣使用這個數據結構進行數據存取的呢?分紅兩個部分:異步
第一步:HashMap內部有一個entry的內部類,其中有四個屬性,咱們要存儲一個值,則須要一個key和一個value,存到map中就會先將key和value保存在這個Entry類建立的對象中。ide
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值來肯定將該對象存放在數組中的哪一個位置上。
上圖很形象的展現了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的數據結構圖來分析:
1)HashMap繼承結構
上面就繼承了一個abstractMap,也就是用來減輕實現Map接口的編寫負擔。
2)實現接口
Map<K,V>:在AbstractMap抽象類中已經實現過的接口,這裏又實現,其實是多餘的。但每一個集合都有這樣的錯誤,也沒過大影響
Cloneable:可以使用Clone()方法,在HashMap中,實現的是淺層次拷貝,即對拷貝對象的改變會影響被拷貝的對象。
Serializable:可以使之序列化,便可以將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; }
有四個構造方法,構造方法的做用就是記錄一下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); } } }
這裏咱們來看一下咱們經常使用的一些方法的源碼
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()
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來取得元素的。
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; }
進行擴容,會伴隨着一次從新hash分配,而且會遍歷hash表中全部的元素,是很是耗時的。在編寫程序中,要儘可能避免resize。
在resize前和resize後的元素佈局以下:
上圖只是針對了數組下標爲2的桶中的各個元素在擴容後的分配佈局,其餘各個桶中的元素佈局能夠以此類推。
從putVal源代碼中咱們能夠知道,當插入一個元素的時候size就加1,若size大於threshold的時候,就會進行擴容。假設咱們的capacity大小爲32,loadFator爲0.75,則threshold爲24 = 32 * 0.75,
此時,插入了25個元素,而且插入的這25個元素都在同一個桶中,桶中的數據結構爲紅黑樹,則還有31個桶是空的,也會進行擴容處理,其實,此時,還有31個桶是空的,好像彷佛不須要進行擴容處理,
可是是須要擴容處理的,由於此時咱們的capacity大小可能不適當。咱們前面知道,擴容處理會遍歷全部的元素,時間複雜度很高;前面咱們還知道,通過一次擴容處理後,元素會更加均勻的分佈在各個桶中,
會提高訪問效率。因此,說盡可能避免進行擴容處理,也就意味着,遍歷元素所帶來的壞處大於元素在桶中均勻分佈所帶來的好處。
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