博文出處:HashMap內(nèi)部原理解析,歡迎大家關(guān)注我的博客,謝謝!
注:本文解析的 HashMap 源代碼基于 Java 1.7 。
Header
HashMap 在平時(shí) Java/Android 開發(fā)中,是絕大多數(shù)開發(fā)者都普遍使用的集合類。
它內(nèi)部是基于哈希表實(shí)現(xiàn)的鍵值對(duì)存儲(chǔ),繼承 AbstractMap 并且實(shí)現(xiàn)了 Map 接口。
而對(duì)于它的 get/put 使用方法相信大家都已經(jīng)到了爐火純青的地步。雖然都會(huì)用,卻可能沒有好好深入探討過 HashMap 內(nèi)部的實(shí)現(xiàn)原理。正好趁著有時(shí)間,今天就給大家一步步地解析 HashMap 的內(nèi)部實(shí)現(xiàn)原理。
在這就基于了 Java 1.7 的源代碼來講解了,Java 1.8 的 HashMap 源碼相比 Java 1.7 做了一些改動(dòng)。具體的改動(dòng)等到我們最后再說。
HashMap 必知
以下是 HashMap 源碼里面的一些關(guān)鍵成員變量以及知識(shí)點(diǎn)。在后面的源碼解析中會(huì)遇到,所以我們有必要先了解下。
- initialCapacity:初始容量。指的是 HashMap 集合初始化的時(shí)候自身的容量??梢栽跇?gòu)造方法中指定;如果不指定的話,總?cè)萘磕J(rèn)值是 16 。需要注意的是初始容量必須是 2 的冪次方。
- size:當(dāng)前 HashMap 中已經(jīng)存儲(chǔ)著的鍵值對(duì)數(shù)量,即
HashMap.size()。 - loadFactor:加載因子。所謂的加載因子就是 HashMap (當(dāng)前的容量/總?cè)萘? 到達(dá)一定值的時(shí)候,HashMap 會(huì)實(shí)施擴(kuò)容。加載因子也可以通過構(gòu)造方法中指定,默認(rèn)的值是 0.75 。舉個(gè)例子,假設(shè)有一個(gè) HashMap 的初始容量為 16 ,那么擴(kuò)容的閥值就是 0.75 * 16 = 12 。也就是說,在你打算存入第 13 個(gè)值的時(shí)候,HashMap 會(huì)先執(zhí)行擴(kuò)容。
- threshold:擴(kuò)容閥值。即 擴(kuò)容閥值 = HashMap 總?cè)萘?* 加載因子。當(dāng)前 HashMap 的容量大于或等于擴(kuò)容閥值的時(shí)候就會(huì)去執(zhí)行擴(kuò)容。擴(kuò)容的容量為當(dāng)前 HashMap 總?cè)萘康膬杀?。比如,?dāng)前 HashMap 的總?cè)萘繛?16 ,那么擴(kuò)容之后為 32 。
- table:Entry 數(shù)組。我們都知道 HashMap 內(nèi)部存儲(chǔ) key/value 是通過 Entry 這個(gè)介質(zhì)來實(shí)現(xiàn)的。而 table 就是 Entry 數(shù)組。
-
在 Java 1.7 中,HashMap 的實(shí)現(xiàn)方法是數(shù)組 + 鏈表的形式。上面的 table 就是數(shù)組,而數(shù)組中的每個(gè)元素,都是鏈表的第一個(gè)結(jié)點(diǎn)。即如下圖所示:
20180114111559
源碼分析
構(gòu)造方法
// 默認(rèn)的構(gòu)造方法使用的都是默認(rèn)的初始容量和加載因子
// DEFAULT_INITIAL_CAPACITY = 16,DEFAULT_LOAD_FACTOR = 0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
// 可以指定初始容量,并且使用默認(rèn)的加載因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
// 對(duì)初始容量的值判斷
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 設(shè)置加載因子
this.loadFactor = loadFactor;
threshold = initialCapacity;
// 空方法
init();
}
HashMap 的所有構(gòu)造方法最后都會(huì)去調(diào)用 HashMap(int initialCapacity, float loadFactor) 。在其內(nèi)部去設(shè)置初始容量和加載因子。而最后的 init() 是空方法。
put 方法
public V put(K key, V value) {
// 如果 table 數(shù)組為空時(shí)先創(chuàng)建數(shù)組,并且設(shè)置擴(kuò)容閥值
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 為空時(shí),調(diào)用 putForNullKey 方法特殊處理
if (key == null)
return putForNullKey(value);
// 計(jì)算 key 的哈希值
int hash = hash(key);
// 根據(jù)計(jì)算出來的哈希值和當(dāng)前數(shù)組的長(zhǎng)度計(jì)算在數(shù)組中的索引
int i = indexFor(hash, table.length);
// 先遍歷該數(shù)組索引下的整條鏈表
// 如果該 key 之前已經(jīng)在 HashMap 中存儲(chǔ)了的話,直接替換對(duì)應(yīng)的 value 值即可
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++;
// 如果該 key 之前沒有被存儲(chǔ)過,那么就進(jìn)入 addEntry 方法
addEntry(hash, key, value, i);
return null;
}
看了上面 put 方法的代碼,大致分為了以下幾個(gè)步驟:
- 如果 table 數(shù)組為空時(shí)先創(chuàng)建數(shù)組,并且設(shè)置擴(kuò)容閥值;
- 如果 key 為空時(shí),調(diào)用 putForNullKey 方法特殊處理;
- 計(jì)算 key 的哈希值;
- 根據(jù)第三步計(jì)算出來的哈希值和當(dāng)前數(shù)組的長(zhǎng)度來計(jì)算得到該 key 在數(shù)組中的索引,其實(shí)索引最后的值就等于
hash%table.length; - 遍歷該數(shù)組索引下的整條鏈表,如果之前已經(jīng)有一樣的 key ,那么直接覆蓋 value ;
- 如果該 key 之前沒有,那么就進(jìn)入 addEntry 方法。
下面就來看一下 addEntry 方法。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 當(dāng)前容量大于或等于擴(kuò)容閥值的時(shí)候,會(huì)執(zhí)行擴(kuò)容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴(kuò)容為原來容量的兩倍
resize(2 * table.length);
// 重新計(jì)算哈希值
hash = (null != key) ? hash(key) : 0;
// 重新得到在新數(shù)組中的索引
bucketIndex = indexFor(hash, table.length);
}
// 創(chuàng)建節(jié)點(diǎn)
createEntry(hash, key, value, bucketIndex);
}
在 addEntry 方法中,有兩個(gè)注意點(diǎn)需要我們?nèi)タ矗?/p>
- 如果當(dāng)前 HashMap 的存儲(chǔ)容量到達(dá)閥值的時(shí)候,會(huì)去進(jìn)行
resize(int newCapacity)擴(kuò)容; - 在 createEntry 方法中增加新的節(jié)點(diǎn)。
我們先去 resize 方法中看看是怎么擴(kuò)容的。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 創(chuàng)建新的 entry 數(shù)組
Entry[] newTable = new Entry[newCapacity];
// 將舊 entry 數(shù)組中的數(shù)據(jù)復(fù)制到新 entry 數(shù)組中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 將新數(shù)組的引用賦給 table
table = newTable;
// 計(jì)算新的擴(kuò)容閥值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
根據(jù)代碼可以知道,擴(kuò)容就是創(chuàng)建了一個(gè)新的數(shù)組,然后把數(shù)據(jù)全部復(fù)制過去,再把新數(shù)組的引用賦給 table 。
剩下的還有一個(gè) 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);
// 當(dāng)前 HashMap 的容量加 1
size++;
}
創(chuàng)建節(jié)點(diǎn)的方法中,如果發(fā)現(xiàn) e 是空的,之前沒有存值,那么直接把值存進(jìn)去就行了;如果是之前 e 有值的,即發(fā)生 hash 碰撞的情況,就以單鏈表頭插入的方式存儲(chǔ)。
get 方法
public V get(Object key) {
// 如果 key 是空的,就調(diào)用 getForNullKey 方法特殊處理
if (key == null)
return getForNullKey();
// 獲取 key 相對(duì)應(yīng)的 entry
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
在 get 方法中,獲取 value 主要步驟是 getEntry(key) 。
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
// 計(jì)算 key 的哈希值
int hash = (key == null) ? 0 : hash(key);
// 得到數(shù)組的索引,然后遍歷鏈表,查看是否有相同 key 的 Entry
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 != null && key.equals(k))))
return e;
}
// 沒有的話,返回 null
return null;
}
getEntry(Object key) 方法很簡(jiǎn)單,就是找到對(duì)應(yīng) key 的數(shù)組索引,然后遍歷鏈表查找即可。
Java 1.8 中 HashMap 的不同
- 在 Java 1.8 中,如果鏈表的長(zhǎng)度超過了 8 ,那么鏈表將轉(zhuǎn)化為紅黑樹;
- 發(fā)生 hash 碰撞時(shí),Java 1.7 會(huì)在鏈表頭部插入,而 Java 1.8 會(huì)在鏈表尾部插入;
- 在 Java 1.8 中,Entry 被 Node 代替(換了一個(gè)馬甲)。
Footer
講完了,現(xiàn)在對(duì) HashMap 應(yīng)該有更深一步的了解了吧,建議大家回去再研究下。
如果哪里有問題或者不懂,可以留言。
bye bye
