本文從HashMap的定義、成員變量、成員方法上詳解HashMap的底層實現。
1 HashMap的定義
1.1 首先來看HashMap的定義
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
......
}
可見HashMap是AbstractMap的子類,並實現了Map接口。其他兩個接口主要是複製和序列化的接口,暫時不管。
1.2 看看AbstractMap類和Map接口長什麼樣
public abstract class AbstractMap<K,V> implements Map<K,V> {}
public interface Map<K,V> {}
Map接口並未繼承其他接口,不同於List和Set接口,Map並未繼承Collection——>Map不能使用迭代器遍歷。
Map接口無非就是定義了一些Map數據結構常用的抽象方法,包括put/remove/get等方法,暫時不去看它。
1.3 粗略的剖析一下AbstractMap抽象類
先找到抽象類中成員變量如下:
transient volatile Set<K> keySet;
transient volatile Collection<V> values;
public abstract Set<Entry<K,V>> entrySet();
上面三個成員變量是其中最重要的三個成員變量,根據變量名就能知道三個貨是幹什麼用的。分別用一個Set存儲所有的key,一個Collection存儲values,和一個Set<Entry>存儲所有的鍵值對。
Entry大概瞭解一下:一個接口,實現類有SimpleEntry/SimpleImmutableEntry。就是一個key、一個value成員變量存儲鍵值對,還包括一些基本方法。
2 HashMap的成員變量
理解HashMap的底層實現,首先看它有哪些成員變量,它用什麼存儲鍵值對?用什麼存儲所有key值?…
首先找到HashMap的所有成員變量(源碼387行開始),把每個成員變量上面的註釋用中文濃縮一下就是:
/* ---------------- Fields -------------- */
//第一次使用時初始化,必要時擴容,每次擴容增大兩倍。(就是存放Node的數組)
transient Node<K,V>[] table;
//爲keySet()和values()使用。(存放所有Entry鍵值對)
transient Set<Map.Entry<K,V>> entrySet;
// 鍵值對個數
transient int size;
//the number of .......
transient int modCount;
//是否resize的閾值
int threshold;
2.1 先談談entrySet的存在。
可見有兩個成員變量存儲鍵值對,一個是table數組,一個是entrySet。那爲什麼有兩個呢?
別忘了HashMap繼承了AbstractMap抽象類,而AbstractMap類中定義了EntrySet抽象方法,返回一個Set<Entry<K,V>>。既然是繼承,就必須實現抽象類中的方法。
HashMap中主體實現並未使用entrySet這個成員變量,所以可以不去考慮它的存在。
2.2 貫穿整個HashMap的Node<K,V>[] table
Map集合的主要功能無非就是增刪改查,而這些操作其實就是在操作table這個成員變量。
2.3 詳解Node
爲什麼不用Entry保存鍵值對,而用Node?
因爲Node裏面多了一個成員變量:next!!
因爲table是用來維持hash表,hash表可能會存在下標衝突,在hash表結構中一般有兩種方式處理下標衝突:1)將數據放在衝突下標的後一位 2)在下標的位置存放鏈表。
既然有next存在,那就是鏈表咯。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
2.4 其他成員變量
其他成員變量在講解成員方法時涉及到時解析。
3 HashMap的成員方法
爲了更好的理解HashMap的底層實現,光了解成員變量還是不行,下面看一下HashMap的put方法,詳解它如何存儲一個鍵值對。
3.1 put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法傳入一個key和value,跟我們使用Map時一樣。主要看一下putVal這個方法的實現。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {}
}
傳入參數有5個,我們先只關注前面三個。hash:key的hash值,調用hash()方法計算得來;key、value沒什麼好說的。那先談談hash這個值的作用。
通俗理解就是:hash這個值就是用來找到鍵值對應該存放在table數組的那個下標位置。我們知道Hash表結構的優點是查詢快,也就是我們能通過一個key值來快速得到value的值,而不是遍歷整個數組。當我們傳入一個key值,先計算key的hash值,通過hash值直接定位到數組下標,如此實現O(1)的時間複雜度查詢。
第一個if語句判斷table是否爲空或長度是否爲0,若爲空或長度爲0則調用resize()方法初始化table數組,初始化數組長度爲16。
第二個if語句中:首先計算 i = (n - 1) & hash;然後得到table[i]的值,然後判斷table[i]是否爲空。若爲空則將鍵值對存放在下標爲i處。i值的計算其實就是根據hash值計算下標,其中n爲數組長度,位與運算保證數組下標不越界。
看完兩個if語句好像搞懂了一些了。那如果計算的下標已經存有其他Node了呢?
代碼就不貼了。簡述一下:Node有一個成員變量爲next,那就在下標出存放一個鏈表,遍歷鏈表存將鍵值對放在鏈表尾部。那麼有個問題就是鏈表太長之後查詢效率不是大大降低麼?爲解決這個問題,源碼將鏈表長度大於8的鏈表轉換成一顆紅黑樹,增加查詢效率。前面說到table初始化長度爲16,當我們存儲大量數據時問題又來了,就算變成紅黑樹效率也低怎麼辦?別忘了threshold這個成員變量是用於擴容的,當size大於threshold時,自動將數組大小增加一倍。
3.2 get方法
通過key值獲取value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && //若table不爲空
(first = tab[(n - 1) & hash]) != null) { //數組相應下標不爲空
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first; //若鏈表頭就是key值,則直接返回Node
if ((e = first.next) != null) { //若鏈表頭的key不能與傳入的key,則遍歷鏈表或樹
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; //未找到則返回null
}