HashMap繼承AbstractMap,實現了Map接口,Map接口定義了所有Map子類必須實現的方法。
1 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap的實例有兩個參數影響其性能:初始容量和加載因子。初始容量只是哈希表在創建時的容量。加載因子是哈希表再其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行rehash操作(擴容操作)。
HashMap中定義的屬性:
1 /** 2 * 默認的初始容量16. 3 */ 4 static final int DEFAULT_INITIAL_CAPACITY = 16; 5 /** 6 * 最大容量 7 */ 8 static final int MAXIMUM_CAPACITY = 1 << 30; 9 /** 10 * 默認裝載因子0.75f. 11 */ 12 static final float DEFAULT_LOAD_FACTOR = 0.75f; 13 /** 14 * 存儲數據的Entry數組 15 */ 16 transient Entry[] table; 17 /** 18 * map中保存的鍵值對的數量 19 */ 20 transient int size; 21 /** 22 * 需要調整大小的極限值(容量*裝載因子) 23 */ 24 int threshold; 25 /** 26 *裝載因子,當HashMap的數據大小>=容量*加載因子時,HashMap會將容量擴容 27 */ 28 final float loadFactor; 29 /** 30 * map結構被改變的次數 31 */ 32 transient volatile int modCount;
HashMap的構造方法。
1 /** 2 *使用默認的容量及裝載因子構造一個空的HashMap 3 */ 4 public HashMap() { 5 this.loadFactor = DEFAULT_LOAD_FACTOR; 6 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); 7 table = new Entry[DEFAULT_INITIAL_CAPACITY];//根據默認容量(16)初始化table 8 init(); 9 } 10 /** 11 * 根據給定的初始容量的裝載因子創建一個空的HashMap 12 * 初始容量小於0或裝載因子小於等於0將報異常 13 */ 14 public HashMap(int initialCapacity, float loadFactor) { 15 if (initialCapacity < 0) 16 throw new IllegalArgumentException("Illegal initial capacity: " + 17 initialCapacity); 18 if (initialCapacity > MAXIMUM_CAPACITY) 19 initialCapacity = MAXIMUM_CAPACITY; 20 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 21 throw new IllegalArgumentException("Illegal load factor: " + 22 loadFactor); 23 int capacity = 1; 24 //設置capacity為大於initialCapacity且是2的冪的最小值 25 while (capacity < initialCapacity) 26 capacity <<= 1; 27 this.loadFactor = loadFactor; 28 threshold = (int)(capacity * loadFactor); 29 table = new Entry[capacity]; 30 init(); 31 } 32 /** 33 *根據指定容量創建一個空的HashMap 34 */ 35 public HashMap(int initialCapacity) { 36 this(initialCapacity, DEFAULT_LOAD_FACTOR);//調用上面的構造方法,容量為指定的容量,裝載因子是默認值 37 } 38 /** 39 *通過傳入的map創建一個HashMap,容量為默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子為默認值 40 */ 41 public HashMap(Map<? extends K, ? extends V> m) { 42 this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, 43 DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); 44 putAllForCreate(m); 45 }
這里需要注意一個小問題,在第二個構造方法中,capacity才是初始容量,而不是initialCapacity,即如果執行new HashMap(9,0.75),那么HashMap的初始容量是16,而不是9。
在初始化table的時候都使用了Entry,這是HashMap的一個內部類。
Map.Entry接口定義的方法
1 K getKey(); 2 V getValue(); 3 V setValue(); 4 boolean equals(Object o); 5 int hashCode();
HashMap.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 final int hash; 6 7 Entry(int h, K k, V v, Entry<K,V> n) { 8 value = v; 9 next = n; 10 key = k; 11 hash = h; 12 } 13 14 public final K getKey() { 15 return key; 16 } 17 18 public final V getValue() { 19 return value; 20 } 21 22 public final V setValue(V newValue) { 23 V oldValue = value; 24 value = newValue; 25 return oldValue;//返回的是之前的Value 26 } 27 28 public final boolean equals(Object o) { 29 if (!(o instanceof Map.Entry))//先判斷類型是否一致 30 return false; 31 Map.Entry e = (Map.Entry)o; 32 Object k1 = getKey(); 33 Object k2 = e.getKey(); 34 // Key相等且Value相等則兩個Entry相等 35 if (k1 == k2 || (k1 != null && k1.equals(k2))) { 36 Object v1 = getValue(); 37 Object v2 = e.getValue(); 38 if (v1 == v2 || (v1 != null && v1.equals(v2))) 39 return true; 40 } 41 return false; 42 } 43 // hashCode是Key的hashCode和Value的hashCode的異或的結果 44 public final int hashCode() { 45 return (key==null ? 0 : key.hashCode()) ^ 46 (value==null ? 0 : value.hashCode()); 47 } 48 // 重寫toString方法,是輸出更清晰 49 public final String toString() { 50 return getKey() + "=" + getValue(); 51 } 52 56 void recordAccess(HashMap<K,V> m) { 57 } 58 62 void recordRemoval(HashMap<K,V> m) { 63 } 64 }
put()
1 public V put(K key, V value) { 2 if (key == null) 3 return putForNullKey(value); 4 int hash = hash(key.hashCode()); 5 int i = indexFor(hash, table.length); 6 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 7 Object k; 8 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 9 V oldValue = e.value; 10 e.value = value; 11 e.recordAccess(this); 12 return oldValue; 13 } 14 } 15 16 modCount++; 17 addEntry(hash, key, value, i); 18 return null; 19 }
當待put的key為null的時候會調用putForNullKey(value)方法,暫且繞過,先來看看如何hash。HashMap並不是直接將對象的hashcode作為哈希值,而是把key的hashcode做一些運算以得到最終的哈希值,而且得到的哈希值也不是在數組中的位置,無論是get還是put還是別的方法,計算哈希值都是:
1 int hash = hash(key.hashCode());
1 static int hash(int h) { 2 // This function ensures that hashCodes that differ only by 3 // constant multiples at each bit position have a bounded 4 // number of collisions (approximately 8 at default load factor). 5 h ^= (h >>> 20) ^ (h >>> 12); 6 return h ^ (h >>> 7) ^ (h >>> 4); 7 }
hash方法為什么要這樣取值,有待探討,但是作用肯定是防止沖突。如何確定數據在數組中的位置:
1 int hash = hash(k.hashCode()); 2 int i = indexFor(hash,table.length);
第一行,得到哈希值,第二行,根據哈希值計算元素在數組中的位置。
1 static int indexFor(int h,int length){ 2 return h & (length - 1); 3 }
"h&(length-1)"其實這里有點小巧妙,為什么是做與運算?
首先我們要確定,HashMap的數組長度永遠是偶數,所以length-1一定是一個奇數,假設現在的長度是16,length-1就是15,對應的二進制是:1111。
假設有兩個元素,一個哈希值是8,二進制是1000,一個哈希值是9,二進制是1001。和1111與運算后,分別還是1000和1001,他們被分配在了數組的不同位置,這樣,哈希的分布非常均勻。
那么如果數組的長度是奇數,減去1后就是偶數了,偶數對應的二進制最低位一定是0,例如14二進制1110。對上面兩個數字分別與運算,得到1000和1000。這樣,哈希值8和9的元素會被存儲在數組同一個位置的鏈表中。在操作的時候,鏈表中的元素越多,效率就會越低,因為要不停的對鏈表循環比較。
在找到元素在數組中的索引位置以后,會循環遍歷table[i]所在的鏈表,如果找到key值與傳入的key值相同的對象,則替換並返回原對象;若找不到,則通過addEntry(hash,key,value,i)添加新的對象。
1 void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; 2 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 3 if (size++ >= threshold) 4 resize(2 * table.length); 5 }
以上過程就是新建一個Entry對象,並放在當前位置的Entry鏈表的頭部。然后判斷size是否達到了需要擴容的界限並讓size增加1,如果達到了擴容的界限則調用resize(int capacity)方法。
1 void resize(int newCapacity) { 2 Entry[] oldTable = table; 3 int oldCapacity = oldTable.length; 4 // 這個if塊表明,如果容量已經到達允許的最大值,即MAXIMUN_CAPACITY,則不再拓展容量,而將裝載拓展的界限值設為計算機允許的最大值。 5 // 不會再觸發resize方法,而是不斷的向map中添加內容,即table數組中的鏈表可以不斷變長,但數組長度不再改變 6 if (oldCapacity == MAXIMUM_CAPACITY) { 7 threshold = Integer.MAX_VALUE; 8 return; 9 } 10 // 創建新數組,容量為指定的容量 11 Entry[] newTable = new Entry[newCapacity]; 12 transfer(newTable); 13 table = newTable; 14 // 設置下一次需要調整數組大小的界限 15 threshold = (int)(newCapacity * loadFactor); 16 }
這里需要重點看看transfer方法:
1 void transfer(Entry[] newTable) { 2 // 保留原數組的引用到src中, 3 Entry[] src = table; 4 // 新容量使新數組的長度 5 int newCapacity = newTable.length; 6 // 遍歷原數組 7 for (int j = 0; j < src.length; j++) { 8 // 獲取元素e 9 Entry<K,V> e = src[j]; 10 if (e != null) { 11 // 將原數組中的元素置為null 12 src[j] = null; 13 // 遍歷原數組中j位置指向的鏈表 14 do { 15 Entry<K,V> next = e.next; 16 // 根據新的容量計算e在新數組中的位置 17 int i = indexFor(e.hash, newCapacity); 18 // 將e插入到newTable[i]指向的鏈表的頭部 19 e.next = newTable[i]; 20 newTable[i] = e; 21 e = next; 22 } while (e != null); 23 } 24 } 25 }
tranfer方法將所有的元素重新哈希,因為新的容量變大,所以每個元素的哈希值和位置都是不一樣的。
如果key值為空,我們來看看putForNullKey的處理過程:
1 private V putForNullKey(V value) { 2 for (Entry<K,V> e = table[0]; e != null; e = e.next) { 3 if (e.key == null) { 4 V oldValue = e.value; 5 e.value = value; 6 e.recordAccess(this); 7 return oldValue; 8 } 9 } 10 modCount++; 11 addEntry(0, null, value, 0); 12 return null; 13 }
這是一個私有方法,在put方法中被調用。它首先遍歷table數組,如果找到key為null的元素,則替換值並返回oldValue;否則通過addEntry方法添加元素,並返回null。
正確的使用HashMap
1.不要再並發場景中使用HashMap
HashMap是線程不安全的,如果被多個線程共享的操作,將會引發不可預知的問題。據sun的說法,在擴容時,會引起鏈表的閉環,在get元素時,就會無限循環。
1 public V get(Object key) { 2 if (key == null) 3 return getForNullKey(); 4 int hash = hash(key.hashCode()); 5 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 6 e != null; 7 e = e.next) { 8 Object k; 9 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 10 return e.value; 11 } 12 return null; 13 }
2.如果數據大小是固定的,那么最好給HashMap設定一個合理的容量值。
本文大量參考借鑒杭州.Mark童鞋HashMap源碼分析這篇文章,如有冒犯,還請見諒。