HashMap是咱們最多見也是最長使用的數據結構之一,它的功能強大、用處普遍。並且也是面試常見的考查知識點。常見問題可能有HashMap存儲結構是什麼樣的?HashMap如何放入鍵值對、如何獲取鍵值對應的值以及如何刪除一個鍵值對。今天咱們就來看看HashMap底層的實現原理。下面咱們就開始進入正題,分析一下hashmap源碼的實現原理。java
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
HashMap的構造方法有好幾個,在這裏咱們就不一一介紹,只說一下咱們最多見的HashMap無參構造方法。上面的構造方法中,有幾個變量須要咱們這裏說明一下:面試
loadFactor:加載因子,默認值爲0.75;算法
threshold:threshold是一個閾值,初始值爲默認爲16*0.75。當hashmap中存放鍵值對數量大於該值時,表示hashmap容量大小須要擴充,通常容量會翻倍。數組
table:table實際上是一個Entry類型的數組,在hashmap中咱們利用數組和鏈表來解決hash衝突,這裏的table數組用於存放衝突鏈表的頭結點。數據結構
另外在HahsMap中,咱們經過數組加鏈表的方式來存儲Entry節點(Entry數據結構用於存儲鍵值對)。這裏所謂的數組便是上面提到的table,它是一個Entry數組,table對象中節點初始化值均爲null,當咱們新插入的節點第一次散列到該位置時,會將節點插入到table中對應位置。若是後續存在散列位置相同的節點,會以鏈表的方式解決hash衝突。示意圖以下:
ide
put方法是咱們最經常使用方法,咱們利用該方法將鍵值對放入HashMap集合中,那麼HashMap究竟是什麼樣的結構,put()方法又作了什麼呢?咱們下面就來看看put()方法的具體實現。this
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方法咱們會在後文中介紹。spa
int hash = hash(key.hashCode());int i = indexFor(hash, table.length);
爲何這裏還要對key的hashCode值再調用一次哈希算法呢?簡單來講就是爲了讓傳遞進來的key散落位置能夠更加均勻,具體緣由就不在本文中介紹了,網上有不少資料可供借鑑。
接着調用indexFor方法計算當前key值散落在table中的位置,其實就是key%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; } }
遍歷以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,則用新值替換老值。orm
若是沒有找到相同的key值,則調用addEntry()方法新增一個指定key和value的Entry節點。
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
Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
此時table[buckertIndex]中存放的節點爲
新建一個Entry節點,key=」key2」,value=」value2」,同時該entry節點next值指向
另外在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()方法具體幹了什麼。
將原來的table數組賦值給src數組
獲取newTable數組的長度,這裏爲table數組長度的2倍
循環遍歷src數組,執行下面的操做
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」,以下圖所示:
最開始的時候newTable[]中並無存聽任何Entry節點,只是單純的進行了初始化。結合上面代碼,咱們能夠看到此時e = entry2節點,next節點值爲entry1
利用indexFor從新計算出e節點的散列位置。e節點的next指向被初始化後的newTable[i]節點,同時newTabel[i]的值也被賦值爲e節點
最後執行e = next;此時e等於entry1
造成節點的示意圖以下:
接着執行
next = e.next,此時e的next節點爲null,next =null;
利用indexFor計算出新的散列位置,好比說新的散列位置爲j,此時以newTable[j]爲頭節點的鏈表中已經存在了兩個節點。以下圖所示:
咱們將待處理的節點entry節點插入後會變成什麼樣呢?
簡單的來講resize方法就是去逐個遍歷table[i]後面的Entry節點鏈表,利用indexFor方法從新結算節點的散落位置,並將其插入到以newTable[]爲頭結點的鏈表中去。
說完了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()方法的實現。
若是key值爲null,執行getForNullKey()方法。當key值爲null時,新的鍵值對會放到table[0]處,因此咱們先去遍歷table[0]位置的節點鏈表,查看是否有key值爲null的節點。若是有的話,直接返回value。若是找不到key爲null的節點,返回null。
若是key值不爲null,利用indexFor方法找到當前key所處的table[i]位置,遍歷table[i]位置的節點鏈表。根據e.hash == hash && ((k = e.key) == key || key.equals(k))來判斷是否有相同key值的節點。若是當前位置鏈表中存在key值相同的Entry節點,返回Entry節點保存的value。若是找不到key值匹配的Entry節點,返回null。
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; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
別看remove方法這麼長,其實它的邏輯很簡單
經過hash()和IndexFor()方法找到當前Entry節點的散列位置i,prev節點爲當前節點的上一個節點(初始值爲table[i]節點),e節點表示當前節點。
比較待刪除節點的key值和當前節點的key值是否相符。若是找不到相符的節點,返回null;
若是有相符的節點,且爲頭結點,e節點的下一個節點將被賦值給table[i];
若是有相匹配的節點,而且不爲頭結點,則prev節點再也不指向e,而是指向e.next,也便是prev.next = e.next;至關於一個斷鏈操做;
若是讓你寫一個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方法:
public Iterator<Map.Entry<K,V>> iterator() { return newEntryIterator(); }
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 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) ; } } }
HashIterator類中有四個屬性,它們的用處代碼註釋已經簡單明瞭的介紹了。值得注意的是HashIterator()提供了一個無參的構造方法,然而他並無對全部的屬性進行初始化,在這裏咱們須要明確的是index的值將會被賦爲0。同時後面還有一大段,它幹了什麼呢?
首先是Entry[] t = table;將當前存儲頭結點的Entry[]數組table賦值給t;
接着執行一個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]的值。
while(iter.hashNext())
當前對象實際上爲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