初識HashMap
關于List荠瘪,ArrayList贡蓖、LinkedList圣贸,CopyOnWriteArrayList殴泰,就前兩者而言于宙,反映的是兩種思想:
(1)ArrayList以數(shù)組形式實現(xiàn),順序插入悍汛、查找快捞魁,插入、刪除較慢
(2)LinkedList以鏈表形式實現(xiàn)离咐,順序插入谱俭、查找較慢奉件,插入、刪除方便
那么是否有一種數(shù)據(jù)結構能夠結合上面兩種的優(yōu)點呢昆著?有县貌,答案就是HashMap。
HashMap是一種非常常見凑懂、方便和有用的集合煤痕,是一種鍵值對(K-V)形式的存儲結構,下面將還是用圖示的方式解讀HashMap的實現(xiàn)原理接谨。
四個關注點在HashMap上的答案
添加數(shù)據(jù)
首先看一下HashMap的一個存儲單元Entry:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
...
}</pre>
從HashMap的Entry看得出摆碉,Entry組成的是一個單向鏈表,因為里面只有Entry的后繼Entry脓豪,而沒有Entry的前驅Entry巷帝。用圖表示應該是這么一個數(shù)據(jù)結構:
接下來,假設我有這么一段代碼:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">public static void main(String[] args)
{
Map<String, String> map = new HashMap<String, String>();
map.put("111", "111");
map.put("222", "222");
}</pre>
看一下做了什么扫夜。首先從第3行開始楞泼,new了一個HashMap出來:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}</pre>
注意一下第5行的init()是個空方法,它是HashMap的子類比如LinkedHashMap構造的時候使用的笤闯。DEFAULT_INITIAL_CAPACITY為16堕阔,也就是說,HashMap在new的時候構造出了一個大小為16的Entry數(shù)組望侈,Entry內所有數(shù)據(jù)都取默認值印蔬,如圖示為:
[圖片上傳中...(image-750d50-1532153177137-3)]
看到new出了一個大小為16的Entry數(shù)組來。接著第4行脱衙,put了一個Key和Value同為111的字符串,看一下put的時候底層做了什么:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">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;
}</pre>
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">static int indexFor(int h, int length) {
return h & (length-1);
}</pre>
看一下put方法的幾個步驟:
1例驹、第2行~第3行就是HashMap允許Key值為空的原因捐韩,空的Key會默認放在第0位的數(shù)組位置上
2、第4行拿到Key值的HashCode鹃锈,由于HashCode是Object的方法荤胁,因此每個對象都有一個HashCode,對這個HashCode做一次hash計算屎债。按照JDK源碼注釋的說法仅政,這次hash的作用是根據(jù)給定的HashCode對它做一次打亂的操作,防止一些糟糕的Hash算法產(chǎn)生的糟糕的Hash值盆驹,至于為什么要防止糟糕的Hash值圆丹,HashMap添加元素的最后會講到
3、第5行根據(jù)重新計算的HashCode躯喇,對Entry數(shù)組的大小取模得到一個Entry數(shù)組的位置辫封∠跬鳎看到這里使用了&,移位加快一點代碼運行效率倦微。另外妻味,這個取模操作的正確性依賴于length必須是2的N次冪,這個熟悉二進制的朋友一定理解欣福,因此注意HashMap構造函數(shù)中责球,如果你指定HashMap初始數(shù)組的大小initialCapacity,如果initialCapacity不是2的N次冪拓劝,HashMap會算出大于initialCapacity的最小2的N次冪的值棕诵,作為Entry數(shù)組的初始化大小。這里為了講解方便凿将,我們假定字符串111和字符串222算出來的i都是1
4校套、第6行~第14行會先判斷一下原數(shù)據(jù)結構中是否存在相同的Key值,存在則覆蓋并返回牧抵,不執(zhí)行后面的代碼笛匙。注意一下recordAccess這個方法,它也是HashMap的子類比如LinkedHashMap用的犀变,HashMap中這個方法為空妹孙。另外,注意一點获枝,對比Key是否相同蠢正,是先比HashCode是否相同,HashCode相同再判斷equals是否為true省店,這樣大大增加了HashMap的效率嚣崭,對HashCode不熟悉的朋友可以看一下我的這篇文章講講HashCode的作用
5、第16行的modeCount++是用于fail-fast機制的懦傍,每次修改HashMap數(shù)據(jù)結構的時候都會自增一次這個值
然后就到了關鍵的addEntry方法了:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">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);
}</pre>
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}</pre>
假設new出來的Entry地址為0×00000001雹舀,那么,put(“111″, “111″)用圖表示應該是這樣的:
[圖片上傳中...(image-b40ba0-1532153177137-2)]
每一個新增的Entry都位于table[1]上粗俱,另外说榆,里面的hash是rehash之后的hash而不是Key最原始的hash〈缛希看到table[1]上存放了111—->111這個鍵值對签财,它持有原table[1]的引用地址,因此可以尋址到原table[1]偏塞,這就是單向鏈表唱蒸。 再看一下put(“222″, “222″)做了什么,一張圖就可以理解了:
新的Entry再次占據(jù)table[1]的位置烛愧,并且持有原table[1]油宜,也就是111—->111這個鍵值對掂碱。
至此,HashMap進行put數(shù)據(jù)的過程就呈現(xiàn)清楚了慎冤。不過還有一個問題疼燥,就是HashMap如何進行擴容,再看一下addEntry方法:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">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);
}</pre>
看到第4行~第5行蚁堤,也就是說在每次放置完Entry之后都會判斷是否需要擴容醉者。這里不講擴容是因為HashMap擴容在不正確的使用場景下將會導致死循環(huán),這是一個值得探討的話題披诗,也是我工作中實際遇到過的一個問題撬即,因此下一篇文章將會詳細說明為什么不正確地使用HashMap會導致死循環(huán)。
刪除數(shù)據(jù)
有一段代碼:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">public static void main(String[] args)
{
Map<String, String> map = new HashMap<String, String>();
map.put("111", "111");
map.put("222", "222");
map.remove("111");
}</pre>
第6行刪除元素呈队,看一下刪除元素的時候做了什么剥槐,第4行~第5行添加了兩個鍵值對就沿用上面的圖,HashMap刪除指定鍵值對的源代碼是:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}</pre>
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">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;
}</pre>
分析一下remove元素的時候做了幾步:
1宪摧、根據(jù)key的hash找到待刪除的鍵值對位于table的哪個位置上
2粒竖、記錄一個prev表示待刪除的Entry的前一個位置Entry,e可以認為是當前位置
3几于、從table[i]開始遍歷鏈表蕊苗,假如找到了匹配的Entry,要做一個判斷沿彭,這個Entry是不是table[i]:
(1)是的話朽砰,也就是第14行~第15行,table[i]就直接是table[i]的下一個節(jié)點喉刘,后面的都不需要動
(2)不是的話瞧柔,也就是第16行~第17行,e的前一個Entry也就是prev饱搏,prev的next指向e的后一個節(jié)點非剃,也就是next,這樣推沸,e所代表的Entry就被踢出了,e的前后Entry就連起來了
remove(“111″)用圖表示就是:
整個過程只需要修改一個節(jié)點的next的值即可券坞,非常方便鬓催。
修改數(shù)據(jù)
修改元素也是put,看一下源代碼:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">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;
}</pre>
這個其實前面已經(jīng)提到過了恨锚,第6行~第14行就是修改元素的邏輯宇驾,如果某個Key已經(jīng)在數(shù)據(jù)結構中存在的話,那么就會覆蓋原value猴伶,也就是第10行的代碼课舍。
插入數(shù)據(jù)
所謂”插入元素”塌西,在我的理解里,一定是基于數(shù)據(jù)結構是有序的前提下的筝尾。像ArrayList捡需、LinkedList,再遠點說就是數(shù)據(jù)庫筹淫,一條一條都是有序的站辉。
而HashMap,它的順序是基于HashCode损姜,HashCode是一個隨機性很強的數(shù)字饰剥,所以HashMap中的Entry完全是隨機存放的。HashMap又不像LinkedHashMap這樣維護了插入元素的順序摧阅,所以對HashMap這個數(shù)據(jù)結構談插入元素是沒有意義的汰蓉。
所以,HashMap并沒有插入的概念棒卷。
再談HashCode的重要性
前面講到了顾孽,HashMap中對Key的HashCode要做一次rehash,防止一些糟糕的Hash算法生成的糟糕的HashCode娇跟,那么為什么要防止糟糕的HashCode岩齿?
糟糕的HashCode意味著的是Hash沖突,即多個不同的Key可能得到的是同一個HashCode苞俘,糟糕的Hash算法意味著的就是Hash沖突的概率增大盹沈,這意味著HashMap的性能將下降,表現(xiàn)在兩方面:
1吃谣、有10個Key乞封,可能6個Key的HashCode都相同,另外四個Key所在的Entry均勻分布在table的位置上岗憋,而某一個位置上卻連接了6個Entry肃晚。這就失去了HashMap的意義,HashMap這種數(shù)據(jù)結構性高性能的前提是仔戈,Entry均勻地分布在table位置上关串,但現(xiàn)在確是1 1 1 1 6的分布。所以监徘,我們要求HashCode有很強的隨機性晋修,這樣就盡可能地可以保證了Entry分布的隨機性,提升了HashMap的效率凰盔。
2墓卦、HashMap在一個某個table位置上遍歷鏈表的時候的代碼:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
看到,由于采用了”&&”運算符户敬,因此先比較HashCode落剪,HashCode都不相同就直接pass了睁本,不會再進行equals比較了。HashCode因為是int值忠怖,比較速度非衬匮撸快,而equals方法往往會對比一系列的內容脑又,速度會慢一些暮胧。Hash沖突的概率大,意味著equals比較的次數(shù)勢必增多问麸,必然降低了HashMap的效率了往衷。
HashMap的table為什么是transient的
一個非常細節(jié)的地方:
transient Entry[] table;
看到table用了transient修飾,也就是說table里面的內容全都不會被序列化严卖,不知道大家有沒有想過這么寫的原因席舍?
在我看來,這么寫是非常必要的哮笆。因為HashMap是基于HashCode的来颤,HashCode作為Object的方法,是native的:
public native int hashCode();
這意味著的是:HashCode和底層實現(xiàn)相關稠肘,不同的虛擬機可能有不同的HashCode算法福铅。再進一步說得明白些就是,可能同一個Key在虛擬機A上的HashCode=1项阴,在虛擬機B上的HashCode=2滑黔,在虛擬機C上的HashCode=3。
這就有問題了环揽,Java自誕生以來略荡,就以跨平臺性作為最大賣點,好了歉胶,如果table不被transient修飾汛兜,在虛擬機A上可以用的程序到虛擬機B上可以用的程序就不能用了,失去了跨平臺性通今,因為:
1粥谬、Key在虛擬機A上的HashCode=100,連在table[4]上
2辫塌、Key在虛擬機B上的HashCode=101帝嗡,這樣,就去table[5]上找Key璃氢,明顯找不到
整個代碼就出問題了。因此狮辽,為了避免這一點一也,Java采取了重寫自己序列化table的方法巢寡,在writeObject選擇將key和value追加到序列化的文件最后面:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">private void writeObject(java.io.ObjectOutputStream s)
throws IOException
{
Iterator<Map.Entry<K,V>> i =
(size > 0) ? entrySet0().iterator() : null;
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
s.writeInt(table.length);
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (i != null) {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
}</pre>
而在readObject的時候重構HashMap數(shù)據(jù)結構:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold, loadfactor, and any hidden stuff
s.defaultReadObject();
// Read in number of buckets and allocate the bucket array;
int numBuckets = s.readInt();
table = new Entry[numBuckets];
init(); // Give subclass a chance to do its thing.
// Read in size (number of Mappings)
int size = s.readInt();
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<size; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}</pre>
一種麻煩的方式,但卻保證了跨平臺性椰苟。
這個例子也告訴了我們:盡管使用的虛擬機大多數(shù)情況下都是HotSpot抑月,但是也不能對其它虛擬機不管不顧,有跨平臺的思想是一件好事舆蝴。
HashMap和Hashtable的區(qū)別
HashMap和Hashtable是一組相似的鍵值對集合谦絮,它們的區(qū)別也是面試常被問的問題之一,我這里簡單總結一下HashMap和Hashtable的區(qū)別:
1洁仗、Hashtable是線程安全的层皱,Hashtable所有對外提供的方法都使用了synchronized,也就是同步赠潦,而HashMap則是線程非安全的
2叫胖、Hashtable不允許空的value,空的value將導致空指針異常她奥,而HashMap則無所謂瓮增,沒有這方面的限制
3、上面兩個缺點是最主要的區(qū)別哩俭,另外一個區(qū)別無關緊要绷跑,我只是提一下,就是兩個的rehash算法不同凡资,Hashtable的是:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}</pre>
這個hashSeed是使用sun.misc.Hashing類的randomHashSeed方法產(chǎn)生的砸捏。HashMap的rehash算法上面看過了,也就是:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 15px; display: block; padding: 12.5px; margin: 0px 0px 13px; line-height: 1.625; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 3px;">static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}</pre>