Java集合——HashMap、HashTable以及ConCurrentHashMap异同比较
轉(zhuǎn)發(fā):https://www.cnblogs.com/zx-bob-123/archive/2017/12/26/8118074.html
0.?前言
?
HashMap和HashTable的區(qū)別一種比較簡(jiǎn)單的回答是:
(1)HashMap是非線程安全的,HashTable是線程安全的。
(2)HashMap的鍵和值都允許有null存在,而HashTable則都不行。
(3)因?yàn)榫€程安全、哈希效率的問(wèn)題,HashMap效率比HashTable的要高。
但是如果繼續(xù)追問(wèn):Java中的另一個(gè)線程安全的與HashMap功能極其類似的類是什么?
同樣是線程安全,它與HashTable在線程同步上有什么不同?帶著這些問(wèn)題,開(kāi)始今天的文章。
本文為原創(chuàng),相關(guān)內(nèi)容會(huì)持續(xù)維護(hù),轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/seu_calvin/article/details/52653711。
?
1.??HashMap概述
?
Java中的數(shù)據(jù)存儲(chǔ)方式有兩種結(jié)構(gòu),一種是數(shù)組,另一種就是鏈表,前者的特點(diǎn)是連續(xù)空間,尋址迅速,但是在增刪元素的時(shí)候會(huì)有較大幅度的移動(dòng),所以數(shù)組的特點(diǎn)是查詢速度快,增刪較慢。
而鏈表由于空間不連續(xù),尋址困難,增刪元素只需修改指針,所以鏈表的特點(diǎn)是查詢速度慢、增刪快。
那么有沒(méi)有一種數(shù)據(jù)結(jié)構(gòu)來(lái)綜合一下數(shù)組和鏈表以便發(fā)揮他們各自的優(yōu)勢(shì)?答案就是哈希表。哈希表的存儲(chǔ)結(jié)構(gòu)如下圖所示:
?
從上圖中,我們可以發(fā)現(xiàn)哈希表是由數(shù)組+鏈表組成的,一個(gè)長(zhǎng)度為16的數(shù)組中,每個(gè)元素存儲(chǔ)的是一個(gè)鏈表的頭結(jié)點(diǎn),通過(guò)功能類似于hash(key.hashCode())%len的操作,獲得要添加的元素所要存放的的數(shù)組位置。
HashMap的哈希算法實(shí)際操作是通過(guò)位運(yùn)算,比取模運(yùn)算效率更高,同樣能達(dá)到使其分布均勻的目的,后面會(huì)介紹。
鍵值對(duì)所存放的數(shù)據(jù)結(jié)構(gòu)其實(shí)是HashMap中定義的一個(gè)Entity內(nèi)部類,數(shù)組來(lái)實(shí)現(xiàn)的,屬性有key、value和指向下一個(gè)Entity的next。
?
?
2.??HashMap初始化
?
HashMap有兩種常用的構(gòu)造方法:
第一種是不需要參數(shù)的構(gòu)造方法:
static final int DEFAULT_INITIAL_CAPACITY = 16; //初始數(shù)組長(zhǎng)度為16 static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量為2的30次方 //裝載因子用來(lái)衡量HashMap滿的程度 //計(jì)算HashMap的實(shí)時(shí)裝載因子的方法為:size/capacity static final float DEFAULT_LOAD_FACTOR = 0.75f; //裝載因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); //默認(rèn)數(shù)組長(zhǎng)度為16 table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }?
第二種是需要參數(shù)的構(gòu)造方法:
public HashMap(int initialCapacity, float loadFactor) { 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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }?
從源碼可以看出,初始化的數(shù)組長(zhǎng)度為capacity,capacity的值總是2的N次方,大小比第一個(gè)參數(shù)稍大或相等。
?
3. ?HashMap的put操作
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; }?
3.1 ?put進(jìn)的key為null
?
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } 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); }從源碼中可以看出,HashMap是允許key為null的,會(huì)調(diào)用putForNullKey()方法:
?
putForNullKey方法會(huì)遍歷以table[0]為鏈表頭的鏈表,如果存在key為null的KV,那么替換其value值并返回舊值。否則調(diào)用addEntry方法,這個(gè)方法也很簡(jiǎn)單,將[null,value]放在table[0]的位置,并將新加入的鍵值對(duì)封裝成一個(gè)Entity對(duì)象,將其next指向原table[0]處的Entity實(shí)例。
?
size表示HashMap中存放的所有鍵值對(duì)的數(shù)量。
threshold = capacity*loadFactor,最后幾行代碼表示當(dāng)HashMap的size大于threshold時(shí)會(huì)執(zhí)行resize操作,將HashMap擴(kuò)容為原來(lái)的2倍。擴(kuò)容需要重新計(jì)算每個(gè)元素在數(shù)組中的位置,indexFor()方法中的table.length參數(shù)也證明了這一點(diǎn)。
但是擴(kuò)容是一個(gè)非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知HashMap中元素的個(gè)數(shù),那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高HashMap的性能。比如說(shuō)我們有1000個(gè)元素,那么我們就該聲明new HashMap(2048),因?yàn)樾枰紤]默認(rèn)的0.75的擴(kuò)容因子和數(shù)組數(shù)必須是2的N次方。若使用聲明new HashMap(1024)那么put過(guò)程中會(huì)進(jìn)行擴(kuò)容。
?
3.2 ?put進(jìn)的key不為null
將上述put方法中的相關(guān)代碼復(fù)制一下方便查看:
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; }?
從源碼可以看出,第1、2行計(jì)算將要put進(jìn)的鍵值對(duì)的數(shù)組的位置i。第4行判斷加入的key是否和以table[i]為鏈表頭的鏈表中所有的鍵值對(duì)有重復(fù),若重復(fù)則替換value并返回舊值,若沒(méi)有重復(fù)則調(diào)用addEntry方法,上面對(duì)這個(gè)方法的邏輯已經(jīng)介紹過(guò)了。
至此HashMap的put操作已經(jīng)介紹完畢了。
?
4. ?HashMap的get操作
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); 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.equals(k))) return e.value; } return null; } private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }?
?
如果了解了前面的put操作,那么這里的get操作邏輯就很容易理解了,源碼中的邏輯已經(jīng)非常非常清晰了。
需要注意的只有當(dāng)找不到對(duì)應(yīng)value時(shí),返回的是null。或者value本身就是null。這是可以通過(guò)containsKey()來(lái)具體判斷。
?
了解了上面HashMap的put和get操作原理,可以通過(guò)下面這個(gè)小例題進(jìn)行知識(shí)鞏固,題目是打印在數(shù)組中出現(xiàn)n/2以上的元素,我們便可以使用HashMap的特性來(lái)解決。
public class HashMapTest { public static void main(String[] args) { int [] a = {2,1,3,2,0,4,2,1,2,3,1,5,6,2,2,3}; Map<Integer, Integer> map = new HashMap<Integer,Integer>(); for(int i=0; i<a.length; i++){ if(map.containsKey(a[i])){ int tmp = map.get(a[i]); tmp+=1; map.put(a[i], tmp); }else{ map.put(a[i], 1); } } Set<Integer> set = map.keySet(); for (Integer s : set) { if(map.get(s)>=a.length/2){ System.out.println(s); } } } }5. ?HashMap和HashTable的對(duì)比
?
HashTable和HashMap采用相同的存儲(chǔ)機(jī)制,二者的實(shí)現(xiàn)基本一致,不同的是:
(1)HashMap是非線程安全的,HashTable是線程安全的,內(nèi)部的方法基本都經(jīng)過(guò)synchronized修飾。
(2)因?yàn)橥健⒐P阅艿仍?#xff0c;性能肯定是HashMap更佳,因此HashTable已被淘汰。
(3)?HashMap允許有null值的存在,而在HashTable中put進(jìn)的鍵值只要有一個(gè)null,直接拋出NullPointerException。
(4)HashMap默認(rèn)初始化數(shù)組的大小為16,HashTable為11。前者擴(kuò)容時(shí)乘2,使用位運(yùn)算取得哈希,效率高于取模。而后者為乘2加1,都是素?cái)?shù)和奇數(shù),這樣取模哈希結(jié)果更均勻。
這里本來(lái)我沒(méi)有仔細(xì)看兩者的具體哈希算法過(guò)程,打算粗略比較一下區(qū)別就過(guò)的,但是最近師姐面試美團(tuán)移動(dòng)開(kāi)發(fā)時(shí)被問(wèn)到了稍微具體一些的算法過(guò)程,我也是醉了…不過(guò)還是恭喜師姐面試成功,起薪20W,真是羨慕,希望自己一年后找工作也能順順利利的。
言歸正傳,看下兩種集合的hash算法。看源碼也不難理解。
//HashMap的散列函數(shù),這里傳入?yún)?shù)為鍵值對(duì)的key static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //返回hash值的索引,h & (length-1)操作等價(jià)于 hash % length操作, 但&操作性能更優(yōu) static int indexFor(int h, int length) { // length must be a non-zero power of 2 return h & (length-1); } //HashTable的散列函數(shù)直接在put方法里實(shí)現(xiàn)了 int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;?
?
?
?
6. ?HashTable和ConCurrentHashMap的對(duì)比
?
先對(duì)ConcurrentHashMap進(jìn)行一些介紹吧,它是線程安全的HashMap的實(shí)現(xiàn)。
HashTable里使用的是synchronized關(guān)鍵字,這其實(shí)是對(duì)對(duì)象加鎖,鎖住的都是對(duì)象整體,當(dāng)Hashtable的大小增加到一定的時(shí)候,性能會(huì)急劇下降,因?yàn)榈鷷r(shí)需要被鎖定很長(zhǎng)的時(shí)間。
ConcurrentHashMap算是對(duì)上述問(wèn)題的優(yōu)化,其構(gòu)造函數(shù)如下,默認(rèn)傳入的是16,0.75,16。
public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2) { //… int i = 0; int j = 1; while (j < paramInt2) { ++i; j <<= 1; } this.segmentShift = (32 - i); this.segmentMask = (j - 1); this.segments = Segment.newArray(j); //… int k = paramInt1 / j; if (k * j < paramInt1) ++k; int l = 1; while (l < k) l <<= 1; for (int i1 = 0; i1 < this.segments.length; ++i1) this.segments[i1] = new Segment(l, paramFloat); } public V put(K paramK, V paramV) { if (paramV == null) throw new NullPointerException(); int i = hash(paramK.hashCode()); //這里的hash函數(shù)和HashMap中的不一樣 return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false); }?
ConcurrentHashMap引入了分割(Segment),上面代碼中的最后一行其實(shí)就可以理解為把一個(gè)大的Map拆分成N個(gè)小的HashTable,在put方法中,會(huì)根據(jù)hash(paramK.hashCode())來(lái)決定具體存放進(jìn)哪個(gè)Segment,如果查看Segment的put操作,我們會(huì)發(fā)現(xiàn)內(nèi)部使用的同步機(jī)制是基于lock操作的,這樣就可以對(duì)Map的一部分(Segment)進(jìn)行上鎖,這樣影響的只是將要放入同一個(gè)Segment的元素的put操作,保證同步的時(shí)候,鎖住的不是整個(gè)Map(HashTable就是這么做的),相對(duì)于HashTable提高了多線程環(huán)境下的性能,因此HashTable已經(jīng)被淘汰了。
?
7. ?HashMap和ConCurrentHashMap的對(duì)比
最后對(duì)這倆兄弟做個(gè)區(qū)別總結(jié)吧:
(1)經(jīng)過(guò)4.2的分析,我們知道ConcurrentHashMap對(duì)整個(gè)桶數(shù)組進(jìn)行了分割分段(Segment),然后在每一個(gè)分段上都用lock鎖進(jìn)行保護(hù),相對(duì)于HashTable的syn關(guān)鍵字鎖的粒度更精細(xì)了一些,并發(fā)性能更好,而HashMap沒(méi)有鎖機(jī)制,不是線程安全的。
(2)HashMap的鍵值對(duì)允許有null,但是ConCurrentHashMap都不允許。
總結(jié)
以上是生活随笔為你收集整理的Java集合——HashMap、HashTable以及ConCurrentHashMap异同比较的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CVTE2016校招试题摘选
- 下一篇: 哪些项目适合写进Java程序员面试简历?