人們對於任何事物的認知,每每都存在這麼一個現象:只有你瞭解的東西,你纔會感興趣。
HashMap之於Java開發者來講,也是如此。相信多數人在起初至關長的時間內,對HashMap的印象都是「Map接口的實現類,是基於哈希的,用於存放鍵-值對,容許null做爲鍵和值,非線程安全的」,僅此而已。因而在程序編寫過程當中便「肆無忌憚」往裏放鍵-值對。而只有你對HashMap的實現有了必定的瞭解以後,你纔會有興趣研究HashMap深層次的問題,好比「HashMap最多能放多少個鍵-值對?如何提升HashMap的使用效率?」。其實,我一直都對HashMap的「線性數組+鏈表」的實現機制充滿好奇,剛剛有時間研究了一下源碼,現把心得與你們分享。java
首先,咱們來看看HashMap的數據結構:數組
/** * 初始容量,大小必須是2的指數次方,默認是16. */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * 默認最大容量,值爲1<<30 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 哈希表的默認加載因子,值爲0.75. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 存儲元素的數組,大小必須是2的指數次方,默認是16. */ transient Entry[] table; /** * THashMap中的存儲的<K,V>映射的數目. */ transient int size; /** * threshold=容量*加載因子。當實際數目大於threshold時,HashMap就須要擴容. * @serial */ int threshold; /** * 哈希表的加載因子,若是建立時不指定loadFactor,則使用DEFAULT_LOAD_FACTOR. * * @serial */ final float loadFactor; //... ... /** * * <p>用一個靜態內部類來定義數組項鍊表的元素</p> * * @author bruce.yang * @version 1.0 Created on 2014-10-22 上午11:39:50 */ static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); }
能夠看出,HashMap正是採用數組(table)類存儲數據的,而數組每個元素則是一個被靜態內部類Entry封裝起來的對象,元素在數組中的下標則是根據key的hashcode計算出來的;當元素下標重複的時候,會在此下標處造成一個鏈表,而這個鏈表就是經過Entry結構來實現的。咱們看到,這個Entry結構是一個單向鏈表,它只有一個next項指向下一個元素,此外,它還包含<K,V>對,同時還有一個哈希值hash。安全
其次,當咱們往hashmap中put元素的時候,先是根據key的hash值計算得出這個元素在數組中的位置(即下標),而後就能夠把這個元素放到對應的位置中了。若是這個位子上已經存放有其餘元素了,那麼在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。源碼:數據結構
/** * 創建指定key和value之間的映射: * 若是已經存在key,則替換對應的value,並返回原來的value;若是原來沒有創建映射,則創建映射,並返回null。 * 固然了,因爲HashMap容許key和value爲null,返回null還有可能就是原來的value爲null。 */ public V put(K key, V value) { //若是key==null,則調用專門的方法處理 if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); 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))) { V oldValue = e.value; e.value = value; e.recordAccess(this); //原來存在這項 return oldValue; } } modCount++; //加入新的項 addEntry(hash, key, value, i); //原來不存在這項 return null; } private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } /** * 添加key-value項 */ void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //加入該項到鏈表頭部 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } /** *擴充HashMap的容量 */ void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } /** * 將原來數組中元素傳輸到新數組中 */ void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
能夠看到,首先判斷key是否爲null:
1).若爲null,則調用專門的方法putForNullKey(value)處理並返回。
1.1)若是事先已經存在key爲null的映射,則替換後返回old value。
1.2)若是不存在,則添加新的項到鏈表中
2).若key不爲null
2.1)首先計算key的哈希值,而後根據哈希值和table數組的長度定位數組項。
2.2)對數組項的鏈表進行遍歷,若是key的哈希值與鏈表中的某一項的哈希值相等且key自己引用值相等或者引用值所指向的對象相等,則替換相應項的value值爲新的value,並返回老的value。若是沒有找到相同的key,則加入該項到鏈表中。
2.3)addEntry方法直接將新的項加入到鏈表的頭部,新項的next引用指向原來的鏈表項。此外判斷是否須要擴容,若是此時存儲的項數目size大於等於threshold,則擴大HashMap容量爲原來的2倍。
2.4)resize方法用來擴容HashMap。默認是擴容至原來的2倍大小。ide
第三,當從hashmap中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素,而後經過key的equals方法在對應位置的鏈表中找到須要的元素。源碼:性能
/** * 根據key取得value */ public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); 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.equals(k))) return e.value; } return null; } private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
能夠看到,若是key爲null,get方法與put方法對應,對null值也有特殊處理,即直接到table[0]中去找key爲null對應的value。
若是key不爲null,則定位key所在數組項,而後遍歷鏈表,若是存在key,則返回對應的value值,不然返回null。this
第四,HashMap的初始化:spa
/** *指定初始化容量和加載因子 */ public HashMap(int initialCapacity, float loadFactor) { //initialCapacity不能小於0 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //若是指定容量大於默認最大容量,則按照默認最大容量建立 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //loadFactor不能小於0 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //找最大於指定初始化容量的2的指數冪做爲表的真實容量 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); } /** *只指定初始化容量,加載因子採用默認 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 初始化容量和加載因子都採用默認 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
只看源碼和註釋就一目瞭然了,這裏所要特別注意一下帶兩個參數的構造方法,其初始化時的初始大小不必定是你所指定的大小。線程
最後,文章開頭提出的那兩個問題已經再也不成爲問題了:
1).HashMap最大容量是1 << 30,
2).而影響HashMap性能的兩個因素是:初始容量和加載因子。這裏借用書中的一段話來表述:容量是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。加載因子是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。
一般,默認加載因子 (0.75)在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數 HashMap類的操做中,包括 get 和 put操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小 rehash操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash操做。
讀到這裏,相信你對HashMap已經有了一個更深入的認識。code
各位,晚安,美夢。