HashMap jdk1.7源码阅读与解析
轉載自??HashMap源碼閱讀與解析
一、導入語
HashMap是我們最常見也是最長使用的數據結構之一,它的功能強大、用處廣泛。而且也是面試常見的考查知識點。常見問題可能有HashMap存儲結構是什么樣的?HashMap如何放入鍵值對、如何獲取鍵值對應的值以及如何刪除一個鍵值對。今天我們就來看看HashMap底層的實現原理。下面我們就開始進入正題,分析一下hashmap源碼的實現原理。
?
二、HashMap構造方法以及存儲結構
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY];init(); }HashMap的構造方法有好幾個,在這里我們就不一一介紹,只說一下我們最常見的HashMap無參構造方法。上面的構造方法中,有幾個變量需要我們這里說明一下:
另外在HahsMap中,我們通過數組加鏈表的方式來存儲Entry節點(Entry數據結構用于存儲鍵值對)。這里所謂的數組即是上面提到的table,它是一個Entry數組,table對象中節點初始化值均為null,當我們新插入的節點第一次散列到該位置時,會將節點插入到table中對應位置。如果后續存在散列位置相同的節點,會以鏈表的方式解決hash沖突。示意圖如下:
?
三、put()方法解析
put方法是我們最常用方法,我們利用該方法將鍵值對放入HashMap集合中,那么HashMap到底是什么樣的結構,put()方法又做了什么呢?我們下面就來看看put()方法的具體實現。
public V put(K key, V value) {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; } if (key == null)return putForNullKey(value);如果當前傳入的key值為null,執行putForNullKey()方法;當key值為null時,hash值為0,將其保存到以table[0]為開頭的鏈表中去。遍歷鏈表,如果存在某節點的key值為null,則用新value直接將其替換。如果未找到key值為null的節點,調用addEntry()方法插入一個key為null的新節點。addEntry方法我們會在后文中介紹。
int hash = hash(key.hashCode()); int i = indexFor(hash, table.length);為什么這里還要對key的hashCode值再調用一次哈希算法呢?簡單來說就是為了讓傳遞進來的key散落位置可以更加均勻,具體原因就不在本文中介紹了,網上有很多資料可供借鑒。
接著調用indexFor方法計算當前key值散落在table中的位置,其實就是key%table.length
遍歷以table[i]為頭結點的鏈表,查找是否已經有相同的key值的節點存在于鏈表中。判斷條件為if (e.hash == hash && ((k = e.key) == key || key.equals(k)))。這個判斷條件十分重要,我們來仔細分析下。首先是e.hash == hash:之前我們已經計算出了當前待處理節點的hash值,并保存在變量hash中,在此我們需要比較當前鏈表遍歷節點key的hash值(e.hash)和hash是否相等。如果我們去看一下addEntry()方法我們會發現,Entry節點的存儲位置實際上是由key的hash值來決定的。如果key的hash相同,那么他們的存儲位置也相同。(k = e.key) == key || key.equals(k))。先簡單的說一下”==”和”equals”的意義,”==”是引用一致性判斷,而equals是內容一致性判斷。這里的意思也就是說如果兩個key對象指向的是同一個對象,或者他們就是同一個對象,則返回true。總結一下,如果hash值相同,則key值相同或是同一個對象的引用,則表示hashmap中存在以key為鍵值的Entry節點。
如果判斷if (e.hash == hash && ((k = e.key) == key || key.equals(k)))判斷條件返回為true,則用新值替換老值。
如果沒有找到相同的key值,則調用addEntry()方法新增一個指定key和value的Entry節點。
?
四、addEntry()方法解析
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); }接下來繼續看addEntry()方法,假設當前節點為插入到table[bucketIndex]位置的第一個節點
Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e);在Entry類的構造方法中有這樣一句代碼:
next = e;即當前新建的entry節點將指向Entry構造方法傳遞過來的Entry節點e,此時e保存的值為頭結點的值,也就是null。該節點創建完之后,又被賦值給table[bucketIndex],相當于鏈表的頭結點了保存了最新插入的節點。如下圖所示我們在table[i]位置插入了Entry節點。
如果此時新來一個key2節點,經過散列之后其散落的位置和key1相同。此時key1和key2的散落位置發生了沖突,我們將采用鏈表來解決該沖突。
還是看那兩句代碼:
示例圖如下圖所示。
我們從上面往hashmap中放鍵值對的過程中可以發現,所有的鍵值對信息其實都是通過Entry節點來保存的,發生沖突的節點會通過一個鏈式結構進行保存。同時table[bucketIndex](相當于頭結點)總是保存最后被放入該位置的鍵值對信息。
另外在addEntry方法中有如下兩句代碼
if (size++ >= threshold)resize(2 * table.length);size的值為當前hashMap中存儲的節點個數,threshold是一個閾值。如果hashMap中存儲的節點個數大于等于threshold,表示我們需要對當前hashMap進行擴容了。每一次擴充容量為之前容量的2倍。我們來看一下resize()方法。
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);}} }關鍵代碼是這一段
Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable;如果resize()之前Entry數組的大小為A,那么newTable數組的大小為2A
transfer(newTable)方法用于將原先entry[]數組中的節點轉移到newTable數組中,下面我們來看下transfer()方法具體干了什么。
a. 取src[j]節點的值賦值給e
b. 如果e節點不為null,將src[j]的值置為null
我們來舉兩個簡單的例子說明一下tranfer到底干了什么:
當src[j]不為空時,比方說src[j]中保存的Entry節點key=”key2”,value=”value2”,src[j]指向的下一個節點key=”key1”,value=”value1”,如下圖所示:
形成節點的示意圖如下:
接著執行
我們將待處理的節點entry節點插入后會變成什么樣呢?
簡單的來說resize方法就是去逐個遍歷table[i]后面的Entry節點鏈表,利用indexFor方法重新結算節點的散落位置,并將其插入到以newTable[]為頭結點的鏈表中去。
?
五、get()方法解析
說完了put我們再來看一下get方法
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; }理解了put方法時如何往hashmap中放入鍵值對的,那么get()方法也就很好理解了。我們來具體看看get()方法的實現。
?
六、remove()方法解析
public V remove(Object key) {Entry<K,V> e = removeEntryForKey(key);return (e == null ? null : e.value); }final Entry<K,V> removeEntryForKey(Object key) {int hash = (key == null) ? 0 : hash(key.hashCode());int i = indexFor(hash, table.length);Entry<K,V> prev = table[i];Entry<K,V> e = prev;while (e != null) {Entry<K,V> next = e.next;Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {modCount++;size--;if (prev == e)table[i] = next;elseprev.next = next;e.recordRemoval(this);return e;}prev = e;e = next;}return e; }別看remove方法這么長,其實它的邏輯很簡單
如果有相符的節點,且為頭結點,e節點的下一個節點將被賦值給table[i];
如果有相匹配的節點,并且不為頭結點,則prev節點不再指向e,而是指向e.next,也即是prev.next = e.next;相當于一個斷鏈操作;
?
七、HashMap遍歷
如果讓你寫一個hashmap的遍歷代碼,估計大部分人寫出下面這段代碼。可是HashMap的遍歷過程到底是怎么樣的,為什么我們每次取值的時候都使用iter.next()來取值的呢?下面我們就來看看HashMap的遍歷實現。
Itreator iter = map.entrySet().itreator();while(iter.hashNext()){Map.entry<k,v> entry = (Map.entry<k,v>) iter.next(); }HashMap類中有一個私有類EntrySet,它繼承自AbstractSet類。EntrySet類中有一個iterator()方法,也就是我們上面在遍歷hashMap所調用的iterator()方法,它會返回一個Iterator對象。
我們來看看iterator方法:
iterator()方法中調用了newEntryIterator()方法,接著進入newEntryIterator()方法看看。
Iterator<Map.Entry<K,V>> newEntryIterator() {return new EntryIterator(); }newEntryIterator方法又創建了一個EntryIterator對象并返回。這個EntryIterator很關鍵,我們來具體看看這個類。
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {public Map.Entry<K,V> next() {return nextEntry();} }EntryIterator類繼承自HashItertor類,而且HashIterator類只有一個方法next()。既然EntryIterator繼承自HashIterator類,那么EntryIterator到底繼承了父類的哪些對象,默認實現了父類的哪些方法呢?我們再看看HashIterator類。
private abstract class HashIterator<E> implements Iterator<E> {Entry<K,V> next; // next entry to returnint expectedModCount; // For fast-failint index; // current slotEntry<K,V> current; // current entryHashIterator() {expectedModCount = modCount;if (size > 0) { // advance to first entryEntry[] t = table;while (index < t.length && (next = t[index++]) == null);}} }HashIterator類中有四個屬性,它們的用處代碼注釋已經簡單明了的介紹了。值得注意的是HashIterator()提供了一個無參的構造方法,然而他并沒有對所有的屬性進行初始化,在這里我們需要明確的是index的值將會被賦為0。同時后面還有一大段,它干了什么呢?
接著執行一個while循環
while (index < t.length && (next = t[index++]) == null)當index大于table的長度,或者當前t[index]位置保存的節點不為空時,將會結束while循環。也就是說該循環目的是為了找出table[]數組中第一個存儲了Entry對象的位置,并用index變量記錄該位置。
我們再總結一下!當Itreator iter = map.entrySet().itreator();這句代碼結束之后,我們獲得了一個Iterator對象,這個對象保存了當前hashMap的modCount值,index用于標識table[]數組中第一個不為null的位置,同時next的初始值也等同于table[index]的值。
當前對象實際上為HashIterator對象,HashIterator對象的hasNext()方法十分的簡單
public final boolean hasNext() {return next != null; } Map.entry<k,v> entry = (Map.entry<k,v>) iter.next();再梳理一下邏輯,EntryIterator 有一個方法next
public Map.Entry<K,V> next() {return nextEntry(); }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; }如果modCount值不等于expectedModCount,表示在當前遍歷過程中,HashMap可能被其他線程修改過,我們需要拋出ConcurrentModificationException異常,這也就是我們常說fast-fail。同時新建一個Entry節點e,賦值為next(第一次進來是next指向的就是table[]數組中第一個不為null的頭結點)。
如果說當前節點的下一個節點為null,相當于遍歷到了當前table[i]所指向鏈表的最后一個節點。此時我們應當去尋找table數組中下一個頭結點不為null的位置。
執行while (index < t.length && (next = t[index++]) == null) 找到下一個不為null的頭結點,并保存到next節點中。
返回當前節點e
?
總結
以上是生活随笔為你收集整理的HashMap jdk1.7源码阅读与解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从来没遇到过这么诡异的电脑售后问题从来没
- 下一篇: 我定的日志规范被CTO在全公司推广了