咱們知道HashMap容許插入元素的key值爲null,咱們看下這部分的源代碼:java
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=null時,對應的數據是保存在內部數組第一個位置的鏈表中。知道了它是如何保存的,那麼獲取也就簡單了:編譯內部數組第一個位置的列表,找到key=null的數據項,返回該數據項中的value便可。算法
private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
在上一篇博文:《基礎知識-HashMap》原理剖析中能夠知道,對於key必須實現其hashCode和equals方法,缺一不可。可是你們知道,這兩個方法默認都是從Object對象中繼承來的,下面看下Object的原生的實現方式:數組
public native int hashCode(); public boolean equals(Object obj) { return (this == obj); }
能夠看到,hashCode方法使用了native關鍵字,表示其實現調用C/C++底層的函數來實現的,而equals方法則認爲只有兩個對象的引用指向同一個對象時,才認爲它們是相等的。併發
若是你自定義了一個類,且沒有從新覆寫equals方法和hashCode方法,而你又使用該類的對象做爲key值保存到HashMap,那麼在讀取HashMap的時候,除非你使用一個與你保存時引用徹底相同的對象做爲key值,不然你再也得不到該key所對應的value。ide
這裏給出良好hashCode和equals的實現例子:函數
public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } @Override public boolean equals(Object o) { if (o == null) return false; if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } @Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; } }
下面給出hashCode的實現建議:
性能
一、把某個非零的常數值,好比17,保存在一個名爲result的int類型的變量中。
二、對於對象中每一個關鍵域f(指equals方法中涉及的每一個域),完成如下步驟:
a、爲該域計算int類型的散列碼c:
i、若是該域是boolean類型,則計算(f?1:0)。
ii、若是該域是byte,char,short或者int類型,則計算(int)f。
iii、若是該域是long類型,則計算(int)(f^(f>>>32))。
iv、若是該域是float類型,則計算Float.floatToIntBits(f)。
v、若是該域是double類型,則計算Double.doubleToLongBits(f),而後按照步驟2.a.iii,爲獲得的long類型值計算散列值。
vi、若是該域是一個對象引用,而且該類的equals方法經過遞歸地調用equals的方式來比較這個域,則一樣爲這個域遞歸地調用hashCode。若是須要更復雜的比較,則爲這個域計算一個範式(canonical representation),而後針對這個範式調用hashCode。若是這個域的值爲null,則返回0(其餘常數也行)。
vii、若是該域是一個數組,則要把每個元素當作單獨的域來處理。也就是說,遞歸地應用上述規則,對每一個重要的元素計算一個散列碼,而後根據步驟2.b中的作法把這些散列值組合起來。若是數組域中的每一個元素都很重要,能夠利用發行版本1.5中增長的其中一個Arrays.hashCode方法。
b、按照下面的公式,把步驟2.a中計算獲得的散列碼c合併到result中:result = 31 * result + c; //此處31是個奇素數,而且有個很好的特性,即用移位和減法來代替乘法,能夠獲得更好的性能:31*i == (i<<5) - i,現代JVM能自動完成此優化。
三、返回result
四、檢驗並測試該hashCode實現是否符合通用約定。
注意:在計算過程當中,冗餘項要排除在外。必須排除能夠經過其餘域值計算出來或equals比較計算中沒用的的任何域,不然有可能違反hashCode第二條約定。測試
HashMap內部維護了一個實例變量modCount,該變量被聲明爲volatile,被volatile聲明的變量表示任何線程均可以看到該變量被其餘線程修改的結果。當使用迭代器(Iterator)進行迭代時,會將modCount的值賦給expectedModCount,在迭代過程當中,經過每次比較二者是否相等來判斷HashMap是否在內部或者被其餘線程修改。而HashMap中不少方法都會改變ModCount,如:put,remove,clear。
優化
先看下HashMap內部迭代器的實現:this
private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } public final boolean hasNext() { return next != null; } final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } current = e; return e; } //... }
從上面的實現能夠看出,HashMap所採用的Fast-Fail機制本質上是一種樂觀鎖機制,經過檢查modCount狀態,沒有問題則忽略,有問題則拋出異常的方式,來避免線程同步的開銷。當咱們在迭代的過程當中,修改了HashMap內部的元素,致使modCount的值改變,代碼就會拋出java.util.ConcurrentModificationException。有意思的是若是HashMap只有一個元素的時候, ConcurrentModificationException 異常並不會被拋出。須要注意的就是:注意,迭代器的快速失敗行爲不能獲得保證,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。所以,編寫依賴於此異常的程序的作法是錯誤的,正確作法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤。
當調用默認的構造函數時:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; //數組的影子 init(); }
table.length=16。
當指定初始容量和加載因子時,源碼以下:
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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; //經過上面這個算法,找到最接近initialCapacity,且又知足2的整數次方 this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; //發現實際容量爲capacity, 並不是參數initialCapacity init(); }
由此看出,構造函數中指定的initialCapacity並不必定是HashMap內部維護數組的初始大小,而永遠都是2的N次方。