HashMap中的數據結構是數組+單鏈表的組合,以鍵值對(key-value)的形式存儲元素的,經過put()和get()方法儲存和獲取對象。java
(方塊表示Entry對象,橫排表示數組table[],縱排表示哈希桶bucket【其實是一個由Entry組成的鏈表,新加入的Entry放在鏈頭,最早加入的放在鏈尾】,)算法
源碼分析:數組
/** 初始容量,默認16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** 最大初始容量,2^30 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** 負載因子,默認0.75,負載因子越小,hash衝突機率越低 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** 初始化一個Entry的空數組 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** 將初始化好的空數組賦值給table,table數組是HashMap實際存儲數據的地方,並不在EMPTY_TABLE數組中 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /** HashMap實際存儲的元素個數 */ transient int size; /** 臨界值(HashMap 實際能存儲的大小),公式爲(threshold = capacity * loadFactor) */ int threshold; /** 負載因子 */ final float loadFactor; /** HashMap的結構被修改的次數,用於迭代器 */ transient int modCount;
源碼分析:安全
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); // 設置負載因子,臨界值此時爲容量大小,後面第一次put時由inflateTable(int toSize)方法計算設置 this.loadFactor = loadFactor; threshold = initialCapacity; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold); putAllForCreate(m); }
put()源碼分析:數據結構
public V put(K key, V value) { // 若是table引用指向成員變量EMPTY_TABLE,那麼初始化HashMap(設置容量、臨界值,新的Entry數組引用) if (table == EMPTY_TABLE) { inflateTable(threshold); } // 若「key爲null」,則將該鍵值對添加到table[0]處,遍歷該鏈表,若是有key爲null,則將value替換。沒有就建立新Entry對象放在鏈表表頭 // 因此table[0]的位置上,永遠最多存儲1個Entry對象,造成不了鏈表。key爲null的Entry存在這裏 if (key == null) return putForNullKey(value); // 若「key不爲null」,則計算該key的哈希值 int hash = hash(key); // 搜索指定hash值在對應table中的索引 int i = indexFor(hash, table.length); // 循環遍歷table數組上的Entry對象,判斷該位置上key是否已存在 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 哈希值相同而且對象相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 若是這個key對應的鍵值對已經存在,就用新的value代替老的value,而後退出! V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 修改次數+1 modCount++; // table數組中沒有key對應的鍵值對,就將key-value添加到table[i]處 addEntry(hash, key, value, i); return null; }
能夠看到,當咱們給put()方法傳遞鍵和值時,HashMap會由key來調用hash()方法,返回鍵的hash值,計算Index後用於找到bucket(哈希桶)的位置來儲存Entry對象。多線程
若是兩個對象key的hash值相同,那麼它們的bucket位置也相同,但equals()不相同,添加元素時會發生hash碰撞,也叫hash衝突,HashMap使用鏈表來解決碰撞問題。併發
分析源碼可知,put()時,HashMap會先遍歷table數組,用hash值和equals()判斷數組中是否存在徹底相同的key對象, 若是這個key對象在table數組中已經存在,就用新的value代替老的value。若是不存在,就建立一個新的Entry對象添加到table[ i ]處。源碼分析
若是該table[ i ]已經存在其餘元素,那麼新Entry對象將會儲存在bucket鏈表的表頭,經過next指向原有的Entry對象,造成鏈表結構(hash碰撞解決方案)。性能
Entry數據結構源碼以下(HashMap內部類):測試
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; /** 指向下一個元素的引用 */ Entry<K,V> next; int hash; /** * 構造方法爲Entry賦值 */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ... ... }
造成單鏈表的核心代碼以下:
/** * 將Entry添加到數組bucketIndex位置對應的哈希桶中,並判斷數組是否須要擴容 */ void addEntry(int hash, K key, V value, int bucketIndex) { // 若是數組長度大於等於容量×負載因子,而且要添加的位置爲null if ((size >= threshold) && (null != table[bucketIndex])) { // 長度擴大爲原數組的兩倍,代碼分析見下面擴容機制 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } /** * 在鏈表中添加一個新的Entry對象在鏈表的表頭 */ void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
(put方法執行過程)
若是兩個不一樣的key的hashcode相同,兩個值對象儲存在同一個bucket位置,要獲取value,咱們調用get()方法,HashMap會使用key的hashcode找到bucket位置,由於HashMap在鏈表中存儲的是Entry鍵值對,因此找到bucket位置以後,會調用key的equals()方法,按順序遍歷鏈表的每一個 Entry,直到找到想獲取的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那HashMap必須循環到最後才能找到該元素。
get()方法源碼以下:
public V get(Object key) { // 若key爲null,遍歷table[0]處的鏈表(實際上要麼沒有元素,要麼只有一個Entry對象),取出key爲null的value if (key == null) return getForNullKey(); // 若key不爲null,用key獲取Entry對象 Entry<K,V> entry = getEntry(key); // 若鏈表中找到的Entry不爲null,返回該Entry中的value return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } // 計算key的hash值 int hash = (key == null) ? 0 : hash(key); // 計算key在數組中對應位置,遍歷該位置的鏈表 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; // 若key徹底相同,返回鏈表中對應的Entry對象 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } // 鏈表中沒找到對應的key,返回null return null; }
咱們能夠看到在HashMap中要找到某個元素,須要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過HashMap的數據結構是數組和鏈表的結合,因此咱們固然但願這個HashMap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表。
源碼分析:
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
HashMap有兩個參數影響其性能:初始容量和負載因子。都可以經過構造方法指定大小。
容量capacity是HashMap中bucket哈希桶(Entry的鏈表)的數量,初始容量只是HashMap在建立時的容量,最大設置初始容量是2^30,默認初始容量是16(必須爲2的冪),解釋一下,當數組長度爲2的n次冪的時候,不一樣的key經過indexFor()方法算得的數組位置相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,get()的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
負載因子loadFactor是HashMap在其容量自動增長以前能夠達到多滿的一種尺度,默認值是0.75。
當HashMapde的長度超出了加載因子與當前容量的乘積(默認16*0.75=12)時,經過調用resize方法從新建立一個原來HashMap大小的兩倍的newTable數組,最大擴容到2^30+1,並將原先table的元素所有移到newTable裏面,從新計算hash,而後再從新根據hash分配位置。這個過程叫做rehash,由於它調用hash方法找到新的bucket位置。
擴容機制源碼分析:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; // 若是以前的HashMap已經擴充打最大了,那麼就將臨界值threshold設置爲最大的int值 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 根據新傳入的newCapacity建立新Entry數組 Entry[] newTable = new Entry[newCapacity]; // 用來將原先table的元素所有移到newTable裏面,從新計算hash,而後再從新根據hash分配位置 transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 再將newTable賦值給table table = newTable; // 從新計算臨界值,擴容公式在這兒(newCapacity * loadFactor) 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; } } }
數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這個操做是極其消耗性能的。因此若是咱們已經預知HashMap中元素的個數,那麼預設初始容量可以有效的提升HashMap的性能。
從新調整HashMap大小,當多線程的狀況下可能產生條件競爭。由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。
HashMap是線程不安全的,在多線程狀況下直接使用HashMap會出現一些莫名其妙不可預知的問題。在多線程下使用HashMap,有幾種方案:
A.在外部包裝HashMap,實現同步機制
B.使用Map m = Collections.synchronizedMap(new HashMap(...));實現同步(官方參考方案,但不建議使用,使用迭代器遍歷的時候修改映射結構容易出錯)
D.使用java.util.HashTable,效率最低(幾乎被淘汰了)
E.使用java.util.concurrent.ConcurrentHashMap,相對安全,效率高(建議使用)
注意一個小問題,HashMap全部集合類視圖所返回迭代器都是快速失敗的(fail-fast),在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自身的 remove 或 add 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。。所以,面對併發的修改,迭代器很快就會徹底失敗。
JDK1.8的HashMap源碼實現和1.7是不同的,有很大不一樣,其底層數據結構也不同,引入了紅黑樹結構。有網友測試過,JDK1.8HashMap的性能要高於JDK1.7 15%以上,在某些size的區域上,甚至高於100%。隨着size的變大,JDK1.7的花費時間是增加的趨勢,而JDK1.8是明顯的下降趨勢,而且呈現對數增加穩定。當一個鏈表長度大於8的時候,HashMap會動態的將它替換成一個紅黑樹(JDK1.8引入紅黑樹大程度優化了HashMap的性能),這會將時間複雜度從O(n)降爲O(logn)。