HashMap是Java中一中很是經常使用的數據結構,也基本是面試中的「必考題」。它實現了基於「K-V」形式的鍵值對的高效存取。JDK1.7以前,HashMap是基於數組+鏈表實現的,1.8之後,HashMap的底層實現中加入了紅黑樹用於提高查找效率。java
HashMap根據存入的鍵值對中的key計算對應的index,也就是它在數組中的存儲位置。當發生哈希衝突時,即不一樣的key計算出了相同的index,HashMap就會在對應位置生成鏈表。當鏈表的長度超過8時,鏈表就會轉化爲紅黑樹。面試
手寫HashMap以前,咱們討論一個小問題:當咱們在HashMap中根據key查找value時,在數組、鏈表、紅黑樹三種狀況下,平均要作多少次比較?數組
在數組中查找時,咱們能夠經過key的hashcode直接計算它在數組中的位置,比較次數爲1數據結構
在鏈表中查找時,根據next引用依次比較各個節點的key,長度爲n的鏈表節點平均比較次數爲n/2ide
在紅黑樹中查找時,因爲紅黑樹的特性,節點數爲n的紅黑樹平均比較次數爲log(n)this
前面咱們提到,鏈表長度超過8時樹化(TREEIFY),正是由於n=8,就是log(n) < n/2的閾值。而n<6時,log(n) > n/2,紅黑樹解除樹化(UNTREEIFY)。另外咱們能夠看到,想要提升HashMap的效率,最重要的就是儘可能避免生成鏈表,或者說盡可能減小鏈表的長度,避免哈希衝突,下降key的比較次數。code
也可使用Java中的java.util.Map
對象
public interface MyMap<K,V> { V put(K k, V v); V get(K k); int size(); V remove(K k); boolean isEmpty(); void clear(); }
而後編寫一個MyHashMap類,實現這個接口,並實現裏面的方法。blog
final static int DEFAULT_CAPACITY = 16; final static float DEFAULT_LOAD_FACTOR = 0.75f; int capacity; float loadFactor; int size = 0; Entry<K,V>[] table;
class Entry<K, V>{ K k; V v; Entry<K,V> next; public Entry(K k, V v, Entry<K, V> next){ this.k = k; this.v = v; this.next = next; } }
咱們參照HashMap設置一個默認的容量capacity和默認的加載因子loadFactor,table就是底層數組,Entry類保存了"K-V"數據,next字段代表它可能會是一個鏈表節點。接口
public MyHashMap(){ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); } public MyHashMap(int capacity, float loadFactor){ this.capacity = upperMinPowerOf2(capacity); this.loadFactor = loadFactor; this.table = new Entry[capacity]; }
這裏的upperMinPowerOf2
的做用是獲取大於capacity的最小的2次冪。在HashMap中,開發者採用了更精妙的位運算的方式完成了這個功能,效率比這種方式要更高。
private static int upperMinPowerOf2(int n){ int power = 1; while(power <= n){ power *= 2; } return power; }
爲何HashMap的capacity必定要是2次冪呢?這是爲了方便HashMap中的數組擴容時已存在元素的從新哈希(rehash)考慮的。
@Override public V put(K k, V v) { // 經過hashcode散列 int index = k.hashCode() % table.length; Entry<K, V> current = table[index]; // 判斷table[index]是否已存在元素 // 是 if(current != null){ // 遍歷鏈表是否有相等key, 有則替換且返回舊值 while(current != null){ if(current.k == k){ V oldValue = current.v; current.v = v; return oldValue; } current = current.next; } // 沒有則使用頭插法 table[index] = new Entry<K, V>(k, v, table[index]); size++; return null; } // table[index]爲空 直接賦值 table[index] = new Entry<K, V>(k, v, null); size++; return null; }
put方法中,咱們經過傳入的K-V值構建一個Entry對象,而後判斷它應該被放在數組的那個位置。回想咱們以前的論斷:
想要提升HashMap的效率,最重要的就是儘可能避免生成鏈表,或者說盡可能減小鏈表的長度
想要達到這一點,咱們須要Entry對象儘量均勻地散佈在數組table中,且index不能超過table的長度,很明顯,取模運算很符合咱們的需求int index = k.hashCode() % table.length
。關於這一點,HashMap中也使用了一種效率更高的方法——經過&運算完成key的散列,有興趣的同窗能夠查看HashMap的源碼。
若是table[index]處已存在元素,說明將要造成鏈表。咱們首先遍歷這個鏈表(長度爲1也視做鏈表),若是存在key與咱們存入的key相等,則替換並返回舊值;若是不存在,則將新節點插入鏈表。插入鏈表又有兩種作法:頭插法
和尾插法
。若是使用尾插法,咱們須要遍歷這個鏈表,將新節點插入末尾;若是使用頭插法,咱們只須要將table[index]的引用指向新節點,而後將新節點的next引用指向原來table[index]位置的節點便可,這也是HashMap中的作法。
若是table[index]處爲空,將新的Entry對象直接插入便可。
@Override public V get(K k) { int index = k.hashCode() % table.length; Entry<K, V> current = table[index]; // 遍歷鏈表 while(current != null){ if(current.k == k){ return current.v; } current = current.next; } return null; }
調用get方法時,咱們根據key的hashcode計算它對應的index,而後直接去table中的對應位置查找便可,若是有鏈表就遍歷。
@Override public V remove(K k) { int index = k.hashCode() % table.length; Entry<K, V> current = table[index]; // 若是直接匹配第一個節點 if(current.k == k){ table[index] = null; size--; return current.v; } // 在鏈表中刪除節點 while(current.next != null){ if(current.next.k == k){ V oldValue = current.next.v; current.next = current.next.next; size--; return oldValue; } current = current.next; } return null; }
移除某個節點時,若是該key對應的index處沒有造成鏈表,那麼直接置爲null。若是存在鏈表,咱們須要將目標節點的前驅節點的next引用指向目標節點的後繼節點。因爲咱們的Entry節點沒有previous引用,所以咱們要基於目標節點的前驅節點進行操做,即:
current.next = current.next.next;
current表明咱們要刪除的節點的前驅節點。
還有一些簡單的size()、isEmpty()等方法都很簡單,這裏就再也不贅述。如今,咱們自定義的MyHashMap基本可使用了。
關於HashMap的實現,還有幾點咱們沒有解決:
NullPointerException
異常。相信你們本身完成了對HashMap的實現以後,對它的原理必定會有更深入的認識,本文若是有錯誤或是不嚴謹的地方也歡迎你們指出。上述的問題咱們接下來再逐步解決,至於紅黑樹,我也不會(攤手)。