hashmap删除指定key_HashTable和HashMap的区别详解
一、HashMap簡介
? ? ? HashMap是基于哈希表實現(xiàn)的,每一個元素是一個key-value對,其內(nèi)部通過單鏈表解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長。
? ? ? HashMap是非線程安全的,只是用于單線程環(huán)境下,多線程環(huán)境下可以采用concurrent并發(fā)包下的concurrentHashMap。
? ? ? HashMap 實現(xiàn)了Serializable接口,因此它支持序列化,實現(xiàn)了Cloneable接口,能被克隆。
? ? ? HashMap存數(shù)據(jù)的過程是:
? ? ? HashMap內(nèi)部維護(hù)了一個存儲數(shù)據(jù)的Entry數(shù)組,HashMap采用鏈表解決沖突,每一個Entry本質(zhì)上是一個單向鏈表。當(dāng)準(zhǔn)備添加一個key-value對時,首先通過hash(key)方法計算hash值,然后通過indexFor(hash,length)求該key-value對的存儲位置,計算方法是先用hash&0x7FFFFFFF后,再對length取模,這就保證每一個key-value對都能存入HashMap中,當(dāng)計算出的位置相同時,由于存入位置是一個鏈表,則把這個key-value對插入鏈表頭。
? ? ? HashMap中key和value都允許為null。key為null的鍵值對永遠(yuǎn)都放在以table[0]為頭結(jié)點的鏈表中。
? ? ??了解了數(shù)據(jù)的存儲,那么數(shù)據(jù)的讀取也就很容易就明白了。
HashMap的存儲結(jié)構(gòu),如下圖所示:
? 圖中,紫色部分即代表哈希表,也稱為哈希數(shù)組,數(shù)組的每個元素都是一個單鏈表的頭節(jié)點,鏈表是用來解決沖突的,如果不同的key映射到了數(shù)組的同一位置處,就將其放入單鏈表中。
? ? ? HashMap內(nèi)存儲數(shù)據(jù)的Entry數(shù)組默認(rèn)是16,如果沒有對Entry擴(kuò)容機(jī)制的話,當(dāng)存儲的數(shù)據(jù)一多,Entry內(nèi)部的鏈表會很長,這就失去了HashMap的存儲意義了。所以HasnMap內(nèi)部有自己的擴(kuò)容機(jī)制。HashMap內(nèi)部有:
? ? ??變量size,它記錄HashMap的底層數(shù)組中已用槽的數(shù)量;
? ? ??變量threshold,它是HashMap的閾值,用于判斷是否需要調(diào)整HashMap的容量(threshold?=?容量*加載因子)????
? ? ??變量DEFAULT_LOAD_FACTOR?=?0.75f,默認(rèn)加載因子為0.75
? ? ? HashMap擴(kuò)容的條件是:當(dāng)size大于threshold時,對HashMap進(jìn)行擴(kuò)容??
? ? ??擴(kuò)容是是新建了一個HashMap的底層數(shù)組,而后調(diào)用transfer方法,將就HashMap的全部元素添加到新的HashMap中(要重新計算元素在新的數(shù)組中的索引位置)。?很明顯,擴(kuò)容是一個相當(dāng)耗時的操作,因為它需要重新計算這些元素在新的數(shù)組中的位置并進(jìn)行復(fù)制處理。因此,我們在用HashMap的時,最好能提前預(yù)估下HashMap中元素的個數(shù),這樣有助于提高HashMap的性能。
? ? ? HashMap共有四個構(gòu)造方法。構(gòu)造方法中提到了兩個很重要的參數(shù):初始容量和加載因子。這兩個參數(shù)是影響HashMap性能的重要參數(shù),其中容量表示哈希表中槽的數(shù)量(即哈希數(shù)組的長度),初始容量是創(chuàng)建哈希表時的容量(從構(gòu)造函數(shù)中可以看出,如果不指明,則默認(rèn)為16),加載因子是哈希表在其容量自動增加之前可以達(dá)到多滿的一種尺度,當(dāng)哈希表中的條目數(shù)超出了加載因子與當(dāng)前容量的乘積時,則要對該哈希表進(jìn)行 resize 操作(即擴(kuò)容)。
? ? ??下面說下加載因子,如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);如果加載因子太小,那么表中的數(shù)據(jù)將過于稀疏(很多空間還沒用,就開始擴(kuò)容了),對空間造成嚴(yán)重浪費(fèi)。如果我們在構(gòu)造方法中不指定,則系統(tǒng)默認(rèn)加載因子為0.75,這是一個比較理想的值,一般情況下我們是無需修改的。
?? ? ??另外,無論我們指定的容量為多少,構(gòu)造方法都會將實際容量設(shè)為不小于指定容量的2的次方的一個數(shù),且最大值不能超過2的30次方
? ? ??對HashMap想進(jìn)一步深入了解的朋友推薦看一下HashMap源碼剖析:http://blog.csdn.net/ns_code/article/details/36034955
二、Hashtable簡介
? ? ? Hashtable同樣是基于哈希表實現(xiàn)的,同樣每個元素是一個key-value對,其內(nèi)部也是通過單鏈表解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長。
? ? ? Hashtable也是JDK1.0引入的類,是線程安全的,能用于多線程環(huán)境中。
? ? ? Hashtable同樣實現(xiàn)了Serializable接口,它支持序列化,實現(xiàn)了Cloneable接口,能被克隆。
? ? ? Hashtable和HashMap比較相似,感興趣的朋友可以看“Hashtable源碼剖析”這篇博客:http://blog.csdn.net/ns_code/article/details/36191279
下面主要介紹一下HashTable和HashMap區(qū)別
三、HashTable和HashMap區(qū)別
? ? ??1、繼承的父類不同
? ? ? Hashtable繼承自Dictionary類,而HashMap繼承自AbstractMap類。但二者都實現(xiàn)了Map接口。
? ? ??2、線程安全性不同
? ? ? javadoc中關(guān)于hashmap的一段描述如下:此實現(xiàn)不是同步的。如果多個線程同時訪問一個哈希映射,而其中至少一個線程從結(jié)構(gòu)上修改了該映射,則它必須保持外部同步。
? ? ? Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情況下是非Synchronize的。在多線程并發(fā)的環(huán)境下,可以直接使用Hashtable,不需要自己為它的方法實現(xiàn)同步,但使用HashMap時就必須要自己增加同步處理。(結(jié)構(gòu)上的修改是指添加或刪除一個或多個映射關(guān)系的任何操作;僅改變與實例已經(jīng)包含的鍵關(guān)聯(lián)的值不是結(jié)構(gòu)上的修改。)這一般通過對自然封裝該映射的對象進(jìn)行同步操作來完成。如果不存在這樣的對象,則應(yīng)該使用 Collections.synchronizedMap 方法來“包裝”該映射。最好在創(chuàng)建時完成這一操作,以防止對映射進(jìn)行意外的非同步訪問,如下所示:
? ? ??Map m = Collections.synchronizedMap(new HashMap(...));
? ? ? Hashtable 線程安全很好理解,因為它每個方法中都加入了Synchronize。這里我們分析一下HashMap為什么是線程不安全的:
? ? ? HashMap底層是一個Entry數(shù)組,當(dāng)發(fā)生hash沖突的時候,hashmap是采用鏈表的方式來解決的,在對應(yīng)的數(shù)組位置存放鏈表的頭結(jié)點。對鏈表而言,新加入的節(jié)點會從頭結(jié)點加入。
我們來分析一下多線程訪問:
? ? ??(1)在hashmap做put操作的時候會調(diào)用下面方法:
//?新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。??????
????void?addEntry(int?hash,?K?key,?V?value,?int?bucketIndex)?{??????
????????//?保存“bucketIndex”位置的值到“e”中??????
????????Entry?e?=?table[bucketIndex];??????
????????//?設(shè)置“bucketIndex”位置的元素為“新Entry”,??????
????????//?設(shè)置“e”為“新Entry的下一個節(jié)點”??????
????????table[bucketIndex]?=?new?Entry(hash,?key,?value,?e);??????
????????//?若HashMap的實際大小?不小于?“閾值”,則調(diào)整HashMap的大小??????
????????if?(size++?>=?threshold)??????
????????????resize(2?*?table.length);??????
????}??
? ? ??在hashmap做put操作的時候會調(diào)用到以上的方法。現(xiàn)在假如A線程和B線程同時對同一個數(shù)組位置調(diào)用addEntry,兩個線程會同時得到現(xiàn)在的頭結(jié)點,然后A寫入新的頭結(jié)點之后,B也寫入新的頭結(jié)點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失
(? ? ??2)刪除鍵值對的代碼
"font-size:?18px;">??????//?刪除“鍵為key”的元素??????
????final?Entry?removeEntryForKey(Object?key)?{??????
????????//?獲取哈希值。若key為null,則哈希值為0;否則調(diào)用hash()進(jìn)行計算??????
????????int?hash?=?(key?==?null)???0?:?hash(key.hashCode());??????
????????int?i?=?indexFor(hash,?table.length);??????
????????Entry?prev?=?table[i];??????
????????Entry?e?=?prev;??????
????????//?刪除鏈表中“鍵為key”的元素??????
????????//?本質(zhì)是“刪除單向鏈表中的節(jié)點”??????
????????while?(e?!=?null)?{??????
????????????Entry?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;??????
????}??
? 當(dāng)多個線程同時操作同一個數(shù)組位置的時候,也都會先取得現(xiàn)在狀態(tài)下該位置存儲的頭結(jié)點,然后各自去進(jìn)行計算操作,之后再把結(jié)果寫會到該數(shù)組位置去,其實寫回的時候可能其他的線程已經(jīng)就把這個位置給修改過了,就會覆蓋其他線程的修改
? ? ??(3)addEntry中當(dāng)加入新的鍵值對后鍵值對總數(shù)量超過門限值的時候會調(diào)用一個resize操作,代碼如下:
//?重新調(diào)整HashMap的大小,newCapacity是調(diào)整后的容量??????
????void?resize(int?newCapacity)?{??????
????????Entry[]?oldTable?=?table;??????
????????int?oldCapacity?=?oldTable.length;?????
????????//如果就容量已經(jīng)達(dá)到了最大值,則不能再擴(kuò)容,直接返回????
????????if?(oldCapacity?==?MAXIMUM_CAPACITY)?{??????
????????????threshold?=?Integer.MAX_VALUE;??????
????????????return;??????
????????}??????
????????//?新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中,??????
????????//?然后,將“新HashMap”賦值給“舊HashMap”。??????
????????Entry[]?newTable?=?new?Entry[newCapacity];??????
????????transfer(newTable);??????
????????table?=?newTable;??????
????????threshold?=?(int)(newCapacity?*?loadFactor);??????
????}??
? ? ??這個操作會新生成一個新的容量的數(shù)組,然后對原數(shù)組的所有鍵值對重新進(jìn)行計算和寫入新的數(shù)組,之后指向新生成的數(shù)組。
? 當(dāng)多個線程同時檢測到總數(shù)量超過門限值的時候就會同時調(diào)用resize操作,各自生成新的數(shù)組并rehash后賦給該map底層的數(shù)組table,結(jié)果最終只有最后一個線程生成的新數(shù)組被賦給table變量,其他線程的均會丟失。而且當(dāng)某些線程已經(jīng)完成賦值而其他線程剛開始的時候,就會用已經(jīng)被賦值的table作為原始數(shù)組,這樣也會有問題。
? ? ??3、是否提供contains方法
? ? ? HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因為contains方法容易讓人引起誤解。
? ? ? Hashtable則保留了contains,containsValue和containsKey三個方法,其中contains和containsValue功能相同。
我們看一下Hashtable的ContainsKey方法和ContainsValue的源碼:
public?boolean?containsValue(Object?value)?{??????
?????return?contains(value);??????
?}??
//?判斷Hashtable是否包含“值(value)”??????
?public?synchronized?boolean?contains(Object?value)?{??????
?????//注意,Hashtable中的value不能是null,??????
?????//?若是null的話,拋出異常!??????
?????if?(value?==?null)?{??????
?????????throw?new?NullPointerException();??????
?????}??????
?????//?從后向前遍歷table數(shù)組中的元素(Entry)??????
?????//?對于每個Entry(單向鏈表),逐個遍歷,判斷節(jié)點的值是否等于value??????
?????Entry?tab[]?=?table;??????
?????for?(int?i?=?tab.length?;?i--?>?0?;)?{??????
?????????for?(Entry?e?=?tab[i]?;?e?!=?null?;?e?=?e.next)?{??????
?????????????if?(e.value.equals(value))?{??????
?????????????????return?true;??????
?????????????}??????
?????????}??????
?????}??????
?????return?false;??????
?}??
//?判斷Hashtable是否包含key??????
?public?synchronized?boolean?containsKey(Object?key)?{??????
?????Entry?tab[]?=?table;??????
/計算hash值,直接用key的hashCode代替????
?????int?hash?=?key.hashCode();????????
?????//?計算在數(shù)組中的索引值?????
?????int?index?=?(hash?&?0x7FFFFFFF)?%?tab.length;??????
?????//?找到“key對應(yīng)的Entry(鏈表)”,然后在鏈表中找出“哈希值”和“鍵值”與key都相等的元素??????
?????for?(Entry?e?=?tab[index]?;?e?!=?null?;?e?=?e.next)?{??????
?????????if?((e.hash?==?hash)?&&?e.key.equals(key))?{??????
?????????????return?true;??????
?????????}??????
?????}??????
?????return?false;??????
?}??
? ? ??下面我們看一下HashMap的ContainsKey方法和ContainsValue的源碼:
//?HashMap是否包含key??????
????public?boolean?containsKey(Object?key)?{??????
????????return?getEntry(key)?!=?null;??????
????}??
//?返回“鍵為key”的鍵值對??????
????final?Entry?getEntry(Object?key)?{??????
????????//?獲取哈希值??????
????????//?HashMap將“key為null”的元素存儲在table[0]位置,“key不為null”的則調(diào)用hash()計算哈希值??????
????????int?hash?=?(key?==?null)???0?:?hash(key.hashCode());??????
????????//?在“該hash值對應(yīng)的鏈表”上查找“鍵值等于key”的元素??????
????????for?(Entry?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;??????
????????}??????
????????return?null;??????
????}??
//?是否包含“值為value”的元素??????
????public?boolean?containsValue(Object?value)?{??????
????//?若“value為null”,則調(diào)用containsNullValue()查找??????
????if?(value?==?null)??????
????????????return?containsNullValue();??????
????//?若“value不為null”,則查找HashMap中是否有值為value的節(jié)點。??????
????Entry[]?tab?=?table;??????
????????for?(int?i?=?0;?i?
????????????for?(Entry?e?=?tab[i]?;?e?!=?null?;?e?=?e.next)??????
????????????????if?(value.equals(e.value))??????
????????????????????return?true;??????
????return?false;??????
????}??
通過上面源碼的比較,我們可以得到第四個不同的地方
? ? ??4、key和value是否允許null值
? ? ??其中key和value都是對象,并且不能包含重復(fù)key,但可以包含重復(fù)的value。
? ? ??通過上面的ContainsKey方法和ContainsValue的源碼我們可以很明顯的看出:
? ? ? Hashtable中,key和value都不允許出現(xiàn)null值。但是如果在Hashtable中有類似put(null,null)的操作,編譯同樣可以通過,因為key和value都是Object類型,但運(yùn)行時會拋出NullPointerException異常,這是JDK的規(guī)范規(guī)定的。HashMap中,null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應(yīng)的值為null。當(dāng)get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應(yīng)的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應(yīng)該用containsKey()方法來判斷。
? ? ??5、兩個遍歷方式的內(nèi)部實現(xiàn)上不同
? ? ? Hashtable、HashMap都使用了 Iterator。而由于歷史原因,Hashtable還使用了Enumeration的方式 。
? ? ??6、hash值不同
? ? ??哈希值的使用不同,HashTable直接使用對象的hashCode。而HashMap重新計算hash值。
? ? ? hashCode是jdk根據(jù)對象的地址或者字符串或者數(shù)字算出來的int類型的數(shù)值。
? ? ? Hashtable計算hash值,直接用key的hashCode(),而HashMap重新計算了key的hash值,Hashtable在求hash值對應(yīng)的位置索引時,用取模運(yùn)算,而HashMap在求位置索引時,則用與運(yùn)算,且這里一般先用hash&0x7FFFFFFF后,再對length取模,&0x7FFFFFFF的目的是為了將負(fù)的hash值轉(zhuǎn)化為正值,因為hash值有可能為負(fù)數(shù),而&0x7FFFFFFF后,只有符號外改變,而后面的位都不變。
? ? ??7、內(nèi)部實現(xiàn)使用的數(shù)組初始化和擴(kuò)容方式不同
? ? ? HashTable在不指定容量的情況下的默認(rèn)容量為11,而HashMap為16,Hashtable不要求底層數(shù)組的容量一定要為2的整數(shù)次冪,而HashMap則要求一定為2的整數(shù)次冪。? ? ? Hashtable擴(kuò)容時,將容量變?yōu)樵瓉淼?倍加1,而HashMap擴(kuò)容時,將容量變?yōu)樵瓉淼?倍。
? ? ? Hashtable和HashMap它們兩個內(nèi)部實現(xiàn)方式的數(shù)組的初始大小和擴(kuò)容的方式。HashTable中hash數(shù)組默認(rèn)大小是11,增加的方式是 old*2+1。
總結(jié)
以上是生活随笔為你收集整理的hashmap删除指定key_HashTable和HashMap的区别详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 距《你的名字》仅一步之遥:新海诚《铃芽之
- 下一篇: c语言goto语句用法_硬件工程师必知的