參考來自:http://blog.csdn.net/jeffleo/article/details/54946424
一 hashMap的基本概念
1.HashMap的定義
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap繼承自AbstractMap,AbstractMap是Map接口的骨幹實現,AbstractMap中實現了Map中最重要最常用和方法。而在這裏仍然實現Map結構,沒有什麼作用,應該是爲了讓map的層次結構更加清晰。
2.HashMap的成員變量
1 int DEFAULT_INITIAL_CAPACITY = 16:默認的初始容量爲16 2 int MAXIMUM_CAPACITY = 1 << 30:最大的容量爲 2 ^ 30 3 float DEFAULT_LOAD_FACTOR = 0.75f:默認的加載因子爲 0.75f 4 Entry<K,V>[] table:Entry類型的數組,HashMap用這個來維護內部的數據結構,它的長度由容量決定 5 int size:HashMap的大小 6 int threshold:HashMap的極限容量,擴容臨界點(容量和加載因子的乘積)
3.HashMap的構造函數
1 public HashMap():構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap 2 public HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap 3 public HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap 4 public HashMap(Map< ? extends K, ? extends V> m):構造一個映射關係與指定 Map 相同的新 HashMap
HashMap 的實例有兩個參數影響其性能:初始容量和加載因子。
初始容量:哈希表在創建時的容量,實際上就是Entry< K,V>[] table的容量。
加載因子 :是哈希表在其容量自動增加之前可以達到多滿的一種尺度。它衡量的是一個散列表的空間的使用程度,加載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果加載因子越大,對空間的利用更充分,然而後果是查找效率的降低;如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。系統默認負載因子爲0.75,一般情況下我們是無需修改的。
當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操作(即重建內部數據結構),從而哈希表將具有大約兩倍的桶數。
二 HashMap的數據結構
1. 數據結構的圖示
從上圖我們可以看出HashMap底層實現還是數組,只是數組的每一項都是一條鏈。其中參數initialCapacity就代表了該數組的長度。
2.HashMap構造函數的源碼
1 public HashMap(int initialCapacity, float loadFactor) { 2 //容量不能小於0 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); 5 //容量不能超出最大容量 6 if (initialCapacity > MAXIMUM_CAPACITY) 7 initialCapacity = MAXIMUM_CAPACITY; 8 //加載因子不能<=0 或者 爲非數字 9 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 10 throw new IllegalArgumentException("Illegal load factor: " + 11 loadFactor); 12 13 //計算出大於初始容量的最小2的n次方作爲哈希表table的長度,下面會說明爲什麼要這樣 14 int capacity = 1; 15 while (capacity < initialCapacity) 16 capacity <<= 1; 17 18 this.loadFactor = loadFactor; 19 //設置HashMap的容量極限,當HashMap的容量達到該極限時就會進行擴容操作 20 threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); 21 //創建Entry數組 22 table = new Entry[capacity]; 23 useAltHashing = sun.misc.VM.isBooted() && 24 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 25 init(); 26 }
可以看到,這個構造函數主要做的事情就是:
1 對傳入的容量和加載因子進行判斷處理 2 設置HashMap的容量極限 3 計算出大於初始容量的最小2的n次方作爲哈希表table的長度,然後用該長度創建Entry數組(table),這個是最核心的。
這裏用到了Entry數組。
Entry是HashMap的一個內部類,它也是維護着一個key-value映射關係,除了key和value,還有next引用(該引用指向當前table位置的鏈表),hash值(用來確定每一個Entry鏈表在table中位置)。
Entry的內部實現如下:
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; 5 int hash; 6 7 /** 8 * Creates new entry. 9 */ 10 Entry(int h, K k, V v, Entry<K,V> n) { 11 value = v; 12 next = n; 13 key = k; 14 hash = h; 15 } 16 }
三 HashMap的存儲實現put(k,v)
1.過程概述
1 傳入key和value,判斷key是否爲null,如果爲null,則調用putForNullKey,以null作爲key存儲到哈希表中; 2 然後計算key的hash值,根據hash值搜索在哈希表table中的索引位置。
3 若當前索引位置不爲null,則對該位置的Entry鏈表進行遍歷。
如果鏈中存在該key,則用傳入的value覆蓋掉舊的value,同時把舊的value返回,結束; 4 否則調用addEntry,用key-value創建一個新的節點,並把該節點插入到該索引對應的鏈表的頭部。
2.源碼解讀
1 public V put(K key, V value) { 2 //如果key爲空的情況 3 if (key == null) 4 return putForNullKey(value); 5 //計算key的hash值 6 int hash = hash(key); 7 //計算該hash值在table中的下標,即計算bucketIndex 8 int i = indexFor(hash, table.length); 9 //對table[i]存放的鏈表進行遍歷 10 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 11 Object k; 12 //判斷該條鏈上是否有hash值相同的(key相同) 13 //若存在相同,則直接覆蓋value,返回舊value 14 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 15 V oldValue = e.value; 16 e.value = value; 17 e.recordAccess(this); 18 return oldValue; 19 } 20 } 21 22 //修改次數+1 23 modCount++; 24 //把當前key,value添加到table[i]的鏈表中 25 addEntry(hash, key, value, i); 26 return null; 27 } 28 }
(1) 如果爲null,則調用putForNullKey:這就是爲什麼HashMap可以用null作爲鍵的原因。
1 private V putForNullKey(V value) { 2 //查找鏈表中是否有null鍵 3 for (Entry<K,V> e = table[0]; e != null; e = e.next) { 4 if (e.key == null) { 5 V oldValue = e.value; 6 e.value = value; 7 e.recordAccess(this); 8 return oldValue; 9 } 10 } 11 modCount++; 12 //如果鏈中查找不到,則把該null鍵插入 13 addEntry(0, null, value, 0); 14 return null; 15 }
(2)如果鏈中存在該key,則用傳入的value覆蓋掉舊的value,同時把舊的value返回:這就是爲什麼HashMap不能有兩個相同的key的原因。
HashMap中,首先計算key的hash值,然後通過hash值獲得bucketIndex。
1 final int hash(Object k) { 2 int h = 0; 3 if (useAltHashing) { 4 if (k instanceof String) { 5 return sun.misc.Hashing.stringHash32((String) k); 6 } 7 h = hashSeed; 8 } 9 10 h ^= k.hashCode(); 11 h ^= (h >>> 20) ^ (h >>> 12); 12 return h ^ (h >>> 7) ^ (h >>> 4); 13 }
1 static int indexFor(int h, int length) { 2 return h & (length-1); 3 }
對於HashMap的table而言,數據分佈需要均勻(最好每項都只有一個元素,這樣就可以直接找到),不能太緊也不能太鬆,太緊會導致查詢速度慢,太鬆則浪費空間。計算hash值後,怎麼才能保證table元素分佈均與呢?
可以採用取模的方式。但是由於取模的消耗較大,HashMap是通過&運算符(按位與操作)來實現的:h & (length-1)。在構造函數中存在:capacity <<= 1,這樣做總是能夠保證HashMap的底層數組長度爲2的n次方。當length爲2的n次方時,h&(length - 1)就相當於對length取模,而且速度比直接取模快得多。即等價不等效。
所以說當length = 2^n時,不同的hash值發生碰撞的概率比較小,這樣就會使得數據在table數組中分佈較均勻,查詢速度也較快。
在獲得bucketIndex之後,調用addEntry將key-value插入到該索引的聯表中。首先取得bucketIndex位置的Entry頭結點,並創建新節點,把該新節點插入到鏈表中的頭部,該新節點的next指針指向原來的頭結點 。
其中addEntry:
1 void addEntry(int hash, K key, V value, int bucketIndex) { 2 //如果size大於極限容量,將要進行重建內部數據結構操作,之後的容量是原來的兩倍,並且重新設置hash值和hash值在table中的索引值 3 if ((size >= threshold) && (null != table[bucketIndex])) { 4 resize(2 * table.length); 5 hash = (null != key) ? hash(key) : 0; 6 bucketIndex = indexFor(hash, table.length); 7 } 8 //真正創建Entry節點的操作 9 createEntry(hash, key, value, bucketIndex); 10 }
1 void createEntry(int hash, K key, V value, int bucketIndex) { 2 Entry<K,V> e = table[bucketIndex]; 3 table[bucketIndex] = new Entry<>(hash, key, value, e); 4 size++; 5 }
系統總是將新的Entry對象添加到bucketIndex處。如果bucketIndex處已經有了對象,那麼新添加的Entry對象將指向原有的Entry對象,形成一條Entry鏈,但是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。
(3)擴容問題
threshold是容器的容量極限,size是HashMap中鍵值對的數量,也就是node的數量。當不斷添加key-value,size大於了容量極限threshold時,會發生擴容。擴容發生在resize方法中,也就是擴大數組(桶)的數量,如何擴容參考:http://blog.csdn.net/jeffleo/article/details/63684953
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
四 HashMap的讀取實現get(key, value)
1.過程概述
1 調用hash(key)求得key的hash值。
2 然後調用indexFor(hash)求得hash值對應的table的索引位置。
3 然後遍歷索引位置的鏈表,如果存在key,則把key對應的Entry返回,否則返回null。
2.源碼解讀
1 public V get(Object key) { 2 //如果key爲null,求null鍵 3 if (key == null) 4 return getForNullKey(); 5 // 用該key求得entry 6 Entry<K,V> entry = getEntry(key); 7 8 return null == entry ? null : entry.getValue(); 9 } 10 11 final Entry<K,V> getEntry(Object key) { 12 int hash = (key == null) ? 0 : hash(key); 13 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { 16 Object k; 17 if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) 19 return e; 20 } 21 return null; 22 }
五 HashMap鍵的遍歷
HashMap遍歷時,按哈希表的每一個索引的鏈表從上往下遍歷,由於HashMap的存儲規則,最晚添加的節點都有可能在第一個索引的鏈表中,這就造成了HashMap的遍歷時無序的。
1 private abstract class HashIterator<E> implements Iterator<E> { 2 Entry<K,V> next; // next entry to return 3 int expectedModCount; // For fast-fail 4 int index; // current slot 5 Entry<K,V> current; // current entry 6 7 //當調用keySet().iterator()時,調用此代碼 8 HashIterator() { 9 expectedModCount = modCount; 10 if (size > 0) { // advance to first entry 11 Entry[] t = table; 12 //從哈希表數組從上到下,查找第一個不爲null的節點,並把next引用指向該節點 13 while (index < t.length && (next = t[index++]) == null); 15 } 16 } 17 18 public final boolean hasNext() { 19 return next != null; 20 } 21 22 //當調用next時,會調用此代碼 23 final Entry<K,V> nextEntry() { 24 if (modCount != expectedModCount) 25 throw new ConcurrentModificationException(); 26 Entry<K,V> e = next; 27 if (e == null) 28 throw new NoSuchElementException(); 29 30 //如果當前節點的下一個節點爲null,從節點處罰往下查找哈希表,找到第一個不爲null的節點 31 if ((next = e.next) == null) { 32 Entry[] t = table; 33 while (index < t.length && (next = t[index++]) == null); 35 } 36 current = e; 37 return e; 38 } 39 40 public void remove() { 41 if (current == null) 42 throw new IllegalStateException(); 43 if (modCount != expectedModCount) 44 throw new ConcurrentModificationException(); 45 Object k = current.key; 46 current = null; 47 HashMap.this.removeEntryForKey(k); 48 expectedModCount = modCount; 49 } 50 }