一. 首先看一下hashmap的數據結構,可以看到是數組加鏈表實現的。
transient Entry<K,V>[] table =(Entry<K,V>[]) EMPTY_TABLE;
可以看到它的實現是一個Entry<K,V>類型的名爲table的數組。而Entry是HashMap中的一個內部類。
static class Entry<K,V> implementsMap.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
它有四個屬性,key,value,next,hash。由於有next屬性,所以自然會想到鏈表的結點類,事實上,當出現hash衝突時,由於HashMap使用鏈地址法來解決衝突。所以table數組的每一個元素就會形成鏈表結構。所以可以說HashMap就是一個存儲鏈表的數組。
二. HashMap的table數組的默認大小是16,並且大小永遠是2的n次方。它還有一個負載因子,默認爲0.75,可以通過帶參數的構造方法自己指定。負載因子loadFactor的作用是:HashMap中的實際的數據大小除以總容量(initialCapacity),當值達到loadFactor時,HashMap的總容量自動擴展一倍。
staticfinal int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
計算threshold,值爲capacity *loadFactor。
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY +1);
這裏就會判斷,當size的值大於threshold(即capacity *loadFactor)時,就會進行擴容。
if ((size >= threshold) && (null != table[bucketIndex])){
resize(2 * table.length);
三.接下來以put方法作爲入口,進行分析。
1.首先進行hash運算,並求出將要存入的數組下標。
int hash = hash(key);
int i = indexFor(hash, table.length);
接下來看看計算下標的算法是如何實現的。進入到indexFor方法中,實現的代碼如下:
static int indexFor(int h, int length) {
// assertInteger.bitCount(length) == 1 : "length must be a non-zero power of2";
return h &(length-1);
}
具體是h &(length-1),這樣計算的值介於0和length-1之間,有點類似於hash%length 的求模運算。之所以用&運算我認爲是位運算的效率更高吧。
2.然後是下面這段代碼:
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);
returnoldValue;
}
}
modCount++;
addEntry(hash, key,value, i);
會判斷table[i]是否爲null,這是會出現兩種情況,先分析第一種情況,即table[i]還沒有元素,是null的情況,這時循環就沒有執行,繼續往下,去執行addEntry方法。addEntry方法中先進行判斷是否需要擴容,如果需要,就進行擴容。然後又進入到createEntry方法中。它的代碼實現如下:
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++;
}
它做的工作就是把hash,key, value, e四個屬性組裝成一個Entry的對象e,並將它放在數組下標相應的位置,這時如果加入的是第一個元素,e則爲null,所以next指向了null。最後再把size加1.
下面分析第二種情況,即即table[i]已經有了元素,不是null的情況。這時會執行上面的那一段for循環,這個循環的作用就是依次遍歷整個table[i]鏈表,並且判斷這個鏈表的每一個元素的key是否和新加進來的元素的key相同,如果相同新的value就會覆蓋舊的value,即保證HashMap中唯一的key有唯一的value.
進行完了覆蓋的操作後,就會執行剩下的代碼,和第一種情況一樣,執行addEntry方法。addEntry方法中先進行判斷是否需要擴容,如果需要,就進行擴容。再執行createEntry方法。這時e = table[bucketIndex];計算出來的e就不爲null了,爲原來的i下標處的元素。然後又封裝一個新的Entry對象,放入到table[i]位置,它的next指向了e,即原來的table[i]處的元素。
所以通過分析我們可以發現,最後放入的元素總是在這個衝突鏈表的表頭的位置。
最後,可以看到,當出現衝突時,會把數據放入鏈表中,每次插入新的元素都會對整個鏈表進行遍歷操作,影響程序的效率。所以當我們向HasnMap中放入的key的數據類型是自定義類型的時候,要按照規範合理的實現hashcode和equals方法,儘量避免衝突。另外,由於它的底層實現也是數組,所以也要儘量避免擴容。最好能估算出初始的大小,而對於負載因子,據說0.75是計算出的最佳值,所以還是用默認的吧。