哈希存储 java_Java容器系列之HashMap的存储
Java容器系列之HashMap
概要
本文將結(jié)合Java源碼總結(jié)HashMap的存儲(chǔ)結(jié)構(gòu)及其擴(kuò)容策略,并根據(jù)這些特點(diǎn)給出使用HashMap的最佳實(shí)踐。
本文不再介紹HashMap的基本使用,有需要的請(qǐng)先學(xué)習(xí)下Java容器的基礎(chǔ)知識(shí)。
存儲(chǔ)結(jié)構(gòu)
HashMap的核心問題是如何保證讀寫的速度?答案是使用Key對(duì)象的Hash值來合理存儲(chǔ)對(duì)象。我們知道,每個(gè)java對(duì)象都有其默認(rèn)的hashCode()方法,也就是每個(gè)對(duì)象都有一個(gè)int值與之對(duì)應(yīng)。那么如何利用這個(gè)int值來快速存儲(chǔ)對(duì)象呢?
1.按照hash值找坑位
假設(shè)定義了一個(gè)長度為8的HashMap,那么HashMap會(huì)創(chuàng)建一個(gè)長度為8的數(shù)組作為容器來存放鍵值對(duì)(在HashMap內(nèi)部使用了Node對(duì)象來存儲(chǔ))。也就是說,這個(gè)數(shù)組的元素是Node對(duì)象。那么,此時(shí)有8個(gè)坑位可以用來放置元素。我們按照J(rèn)ava的數(shù)組index標(biāo)號(hào),也就是從0到7分別標(biāo)號(hào)。
Key對(duì)象的Hash值我們是能夠得到的,此時(shí)如果把這個(gè)Hash值來對(duì)8取模,自然是從0到7的一個(gè)分布了。故根據(jù)Key對(duì)象的Hash值,我們可以快速找到該Key放在了數(shù)組的哪個(gè)坑位,避免的數(shù)組的輪詢。
總結(jié)下,坑位的取得方式,簡單的說就是先得到Key的HashCode,然后把這個(gè)值對(duì)HashMap的長度取模。
此處有一個(gè)問題,如果兩個(gè)不同的Hash值,取模的結(jié)果相等怎么辦?比如: 如果Key1的hash值是7,Key2的hash值是15,此時(shí)它們對(duì)8取模,都是得到7,不可能把這兩個(gè)對(duì)象放在一個(gè)坑位。
2.當(dāng)不同的Key得到同一個(gè)坑位后,按照鏈表或者紅黑樹存儲(chǔ)
JDK8之前,使用了鏈表來存儲(chǔ)hash取模后一樣的元素。而JDK8開始使用了鏈表和紅黑樹相結(jié)合的方式來存儲(chǔ)。
鏈表存儲(chǔ)
查看Node的定義,可以看到它包含了一個(gè)next屬性。
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
......
}
正是利用這個(gè)next,實(shí)現(xiàn)了鏈表的存儲(chǔ)。此時(shí),從HashMap取元素的時(shí)候,首先找到坑位,然后遍歷坑位中的鏈表,逐一判斷Key對(duì)象的hashCode是否相等,而且Key對(duì)象的equals方法是否返回true。當(dāng)且僅當(dāng)hashCode相等,而且equals方法返回true時(shí),才認(rèn)為找到了key對(duì)應(yīng)的元素,并返回該Node對(duì)象中包含的value對(duì)象。
總結(jié)下,一個(gè)坑位下掛載了若干個(gè)元素,這些元素有以下特點(diǎn)。
1)同一坑位下的鏈表中,各個(gè)元素的Key對(duì)象Hash值取模后的值都是一樣的。
2)鏈表的先后順序按照進(jìn)入HashMap的時(shí)間順序排列,新元素被放入坑位,并且其next指向原來在坑位中的元素。
3)這個(gè)順序是可能變化的,比如擴(kuò)容的時(shí)候。這樣也就不難理解為什么HashMap無法保證元素的排序了。
顯然,這樣有個(gè)缺點(diǎn),就是如果這個(gè)鏈表很長,那邊取元素的性能會(huì)隨著鏈表的拉長而變差,而且是越來越差。所以,JDK8開始,引入了紅黑樹的存儲(chǔ)方式。當(dāng)某個(gè)坑位下的鏈表的長度小于8的時(shí)候,還是鏈表存儲(chǔ);否則,變換成紅黑樹存儲(chǔ)。
紅黑樹存儲(chǔ)
紅黑樹能夠保證存取元素的速度不會(huì)隨著元素?cái)?shù)量的增加而迅速惡化(時(shí)間復(fù)雜度為lg(n))。具體這里不展開講,紅黑樹的資料還是很多的。
查看JDK8的HashMap源碼,其putVal方法如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 取模使用的是長度減1再跟hash位與操作
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //達(dá)到8則轉(zhuǎn)換成樹
treeifyBin(tab, hash);// 鏈表轉(zhuǎn)換成紅黑樹
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
擴(kuò)容策略
HashMap在使用的時(shí)候并沒有人會(huì)關(guān)注其大小,看上去這個(gè)容器是無限大小的,反正有東西就往里面放,至于里面放了多少,還能放多少,沒人關(guān)心。這完全得益于其自動(dòng)化的擴(kuò)容機(jī)制。HashMap內(nèi)部定義了一套擴(kuò)容的機(jī)制,以應(yīng)對(duì)元素的擴(kuò)張。
1.HashMap的擴(kuò)容閾值
當(dāng)往HashMap中放入元素的時(shí)候,如果HashMap中的元素個(gè)數(shù)達(dá)到某個(gè)閾值時(shí),會(huì)觸發(fā)擴(kuò)容操作。這個(gè)閾值由loadFactor屬性控制的,這個(gè)值是個(gè)小數(shù)(默認(rèn)值是0.75),閾值等于map的整體容量乘以loadFactor。如此做的目的是確保坑位的量足夠,讓元素能夠足夠的分散。擴(kuò)容后的容量一般是翻倍,而且容量都是2的冪,比如2,4,8,16,32,64.......這樣做的目的是為了方便取模的操作(取模的操作很巧妙,會(huì)再開一文)。
2.HashMap的擴(kuò)容方法
擴(kuò)容的操作通常是增加容量后,重新安置各個(gè)元素。擴(kuò)容后,元素的放置都重新打亂了,等于是重新生成了一次HashMap。所以,盡量減少擴(kuò)容是比較明智的。
最佳實(shí)踐
使用HashMap的時(shí)候,如果能夠預(yù)估容器的大小,那么在初始化的時(shí)候就指定size的大小。這樣可以盡量減少容器的擴(kuò)容操作,提高程序運(yùn)行效率。下例中,我們?cè)诔跏蓟痬ap的時(shí)候,指定了其size為100。如下所示:
Map map = new HashMap(100);
雖然代碼中定義的長度是100,但是實(shí)際的長度是大于100的最小的2的n次方的值,有點(diǎn)繞口。舉例如下:
如果指定了50,那么實(shí)際長度是2的6次方,64
如果指定了100,那么實(shí)際長度是2的7次方,128
以此類推。。。
2.HashMap不是線程安全的,需要注意多線程的同步問題。特別是可能導(dǎo)致死循環(huán)的問題(在HashMap擴(kuò)容時(shí),多線程的情況下使用HashMap可能會(huì)導(dǎo)致死循環(huán))。如果需要保證線程安全,建議使用HashMap+Collections工具類的組合來做,而不是使用Hashtable。如下所示:
Map map = new HashMap();
Map synMap = Collections.synchronizedMap(map);
synMap.put("key1", "value1");
當(dāng)然,Hashtable也是線程安全的,但是該類的同步控制粒度比較大,跟上面比性能更低些,而且該類也是逐步被廢棄的態(tài)勢在發(fā)展,少用吧。
線程安全是消耗性能的,所以能回避線程安全問題就盡量回避,這是上上策。
3.如果還想要保證順序,可以考慮使用LinkedHashMap來實(shí)現(xiàn)。它是在HashMap的基礎(chǔ)上增加了一組保存先后順序的雙向鏈表。可以按照LRU(最不常用的放到最后,越常用的越在前面),或者按照放入容器的順序排列。
總結(jié)
以上是生活随笔為你收集整理的哈希存储 java_Java容器系列之HashMap的存储的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaWbe学习总结之jQuery
- 下一篇: java原生方法,Java Servle