Java集合:HashMap
各種Map總結
-
就比如問你?HashMap 是不是有序的?你回答不是有序的。
-
那面試官就會可能繼續問你,有沒有有序的Map實現類呢?你如果這個時候說不知道的話,那這塊問題就到此結束了。如果你說有 TreeMap 和 LinkedHashMap。
-
那么面試官接下來就可能會問你,TreeMap 和 LinkedHashMap 是如何保證它的順序的?如果你回答不上來,那么到此為止。如果你說
-
TreeMap 是通過實現 SortMap 接口,基于紅黑樹,能夠把它保存的鍵值對根據 key 排序,從而保證 TreeMap 中所有鍵值對處于有序狀態。TreeMap的所有key都必須實現Comparable接口,所有key必須是同一類型。自定義類型需要重寫equals方法,且返回值要和compareTo保持一致。
-
LinkedHashMap 則是通過維護一個雙向鏈表,使用插入排序(就是你 put 的時候的順序是什么,取出來的時候就是什么樣子)和訪問排序(改變排序把訪問過的放到底部)讓鍵值有序。也由于需要維護元素的插入順序,所以性能較之HashMap要差一些。
-
那么面試官還會繼續問你,你覺得它們兩個哪個的有序實現比較好?如果你依然可以回答的話,那么面試官會繼續問你,你覺得還有沒有比它更好或者更高效的實現方式?
有什么方法可以減少hash碰撞?
1.擾動函數可以減少碰撞
原理是如果兩個不相等的對象返回不同的 hashcode 的話,那么碰撞的幾率就會小些。這就意味著存鏈表結構減小,這樣取值的話就不會頻繁調用 equal 方法,從而提高 HashMap 的性能(擾動即 Hash 方法內部的算法實現,目的是讓不同對象返回不同 hashcode)。
2.使用不可變的、聲明作 final 對象,并且采用合適的 equals() 和 hashCode() 方法,將會減少碰撞的發生
不可變性使得能夠緩存不同鍵的 hashcode,這將提高整個獲取對象的速度,使用 String、Integer 這樣的 wrapper 類作為鍵是非常好的選擇。
為什么 String、Integer 這樣的 wrapper 類適合作為鍵?
因為 String 是 final,而且已經重寫了 equals() 和 hashCode() 方法了。不可變性是必要的,因為為了要計算 hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的 hashcode 的話,那么就不能從 HashMap 中找到你想要的對象。
3.開放定址法
當沖突發生時,使用某種探查技術在散列表中形成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定的地址。按照形成探查序列的方法不同,可將開放定址法區分為線性探查法、二次探查法、雙重散列法等。
例子:已知一組關鍵字為 (26,36,41,38,44,15,68,12,06,51),用除余法構造散列函數,用線性探查法解決沖突構造這組關鍵字的散列表。
解答:為了減少沖突,通常令裝填因子 α 由除余法因子是13的散列函數計算出的上述關鍵字序列的散列地址為 (0,10,2,12,5,2,3,12,6,12)。
? ? ? ?前5個關鍵字插入時,其相應的地址均為開放地址,故將它們直接插入 T[0]、T[10)、T[2]、T[12] 和 T[5] 中。
? ? ? ?當插入第6個關鍵字15時,其散列地址2(即 h(15)=15%13=2)已被關鍵字 41(15和41互為同義詞)占用。故探查 h1=(2+1)%13=3,此地址開放,所以將 15 放入 T[3] 中。
? ? ? ?當插入第7個關鍵字68時,其散列地址3已被非同義詞15先占用,故將其插入到T[4]中。
? ? ?當插入第8個關鍵字12時,散列地址12已被同義詞38占用,故探查 hl=(12+1)%13=0,而 T[0] 亦被26占用,再探查 h2=(12+2)%13=1,此地址開放,可將12插入其中。
? ? ??類似地,第9個關鍵字06直接插入 T[6] 中;而最后一個關鍵字51插人時,因探查的地址 12,0,1,…,6 均非空,故51插入 T[7] 中。
HashMap 中 hash 函數怎么是實現的?
我們可以看到,在 hashmap 中要找到某個元素,需要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。
前面說過,hashmap 的數據結構是數組和鏈表的結合,所以我們當然希望這個 hashmap 里面的元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個。那么當我們用 hash 算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。 所以,我們首先想到的就是把 hashcode 對數組長度取模運算。這樣一來,元素的分布相對來說是比較均勻的。
但是“模”運算的消耗還是比較大的,能不能找一種更快速、消耗更小的方式?我們來看看 JDK1.8 源碼是怎么做的(被樓主修飾了一下)
?
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}?
簡單來說就是:
-
高16 bit 不變,低16 bit 和高16 bit 做了一個異或(得到的 hashcode 轉化為32位二進制,前16位和后16位低16 bit 和高16 bit 做了一個異或)
-
(n-1) & hash? ?得到hash桶的下標??
h%length與h&(length-1)得到的結果其實是一個值,但是為什么hashmap中要用后者呢
1.length(2的整數次冪)的特殊性導致了length-1的特殊性(二進制全為1)
2.位運算快于十進制運算,hashmap擴容也是按位擴容,所以相比較就選擇了后者
?
你能談談他的容量問題嗎?
我:hashmap的初始容量是16,內部有一個負載因子初始為0.75,當數組中元素達到初始值*負載因子時,就會進行擴容。方法是使用一個新的數組,數組大小為原來的兩倍。當然負載因子的大小是可調的,當我們有足夠的內存而且非常看重時間的時候,可以將負載因子減小一點,反之可以增加負載因子。
那為什么初始容量是16?
·? 我:在計算數組索引位置的時候, hash值&(數組長度-1)得到所在數組的index
采用兩次hash? ? 第一次是調用hashcode()方法得到h,第二次是用h&(length-1)。
當length為偶數的時候(length-1)得到的二進制的最后一位為1,&運算后即能得到奇數又能得到偶數。而length為奇數僅僅能得到偶數,這樣浪費了一半空間。
這里擴容為啥是2倍?
因為2倍的話,更加容易計算他們所在的桶,并且各自不會相互干擾。
如原桶長度是4,現在桶長度是8,那么
·????????桶0中的元素會被分到桶0和桶4中
·????????桶1中的元素會被分到桶1和桶5中
·????????桶2中的元素會被分到桶2和桶6中
·????????桶3中的元素會被分到桶3和桶7中
為啥是這樣呢?
桶0中的元素的hash值后2位必然是00,這些hash值可以根據后3位000或者100分成2類數據(0,4一類,8,12一類)。他們分別&(8-1)即&111,則后3位為000的在桶0中,后3位為100的必然在桶4中。其他同理,也就是說桶4和桶0重新瓜分了原來桶0中的元素。
如果換成其他倍數,那么瓜分就比較混亂了。
這樣在瓜分這些數據的時候,只需要先把這些數據分類,如上述桶0中分成000和100 2類,然后直接構成新的鏈表,分類完畢后,直接將新的鏈表掛在對應的桶下即可
當length為合數[二的冪方]時h& (length-1)運算等價于對length取模,也就是h%length,但是&比%具有更高的效率,這樣保證了索引能均勻分布。
取16是為了增加桶數組的利用率和減少hash碰撞,還有提升擴容時分配元素的方便性。
HashMap在JDK1.8及以后的版本中引入了紅黑樹結構,若桶中鏈表元素個數大于等于8時,鏈表轉換成樹結構;若桶中鏈表元素個數小于等于6時,樹結構還原成鏈表。
為什么是8?為什么是6?
因為紅黑樹的平均查找長度是log(n),長度為8的時候,平均查找長度為3,如果繼續使用鏈表,平均查找長度為8/2=4,這才有轉換為樹的必要。鏈表長度如果是小于等于6,6/2=3,雖然速度也很快的,但是轉化為樹結構和生成樹的時間并不會太短。
還有選擇6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小于8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。
使用鏈表,平均查找長度?
最好情況是1,最差情況是n,所以平均是(1+n)/2
拉鏈法導致的鏈表過深,為什么不用二叉查找樹代替而選擇紅黑樹?為什么不一直使用紅黑樹?
?
之所以選擇紅黑樹是為了解決二叉查找樹的缺陷:二叉查找樹在特殊情況下會變成一條線性結構(左斜樹,右斜樹等,這就跟原來使用鏈表結構一樣了,造成層次很深的問題),遍歷查找會非常慢。而紅黑樹在插入新數據后可能需要通過左旋、右旋、變色這些操作來保持平衡。引入紅黑樹就是為了查找數據快,解決鏈表查詢深度的問題。我們知道紅黑樹屬于平衡二叉樹,為了保持“平衡”是需要付出代價的,但是該代價所損耗的資源要比遍歷線性鏈表要少。所以當長度大于8的時候,會使用紅黑樹;如果鏈表長度很短的話,根本不需要引入紅黑樹,引入反而會慢。
Hashtable
-
數組 + 鏈表方式存儲
-
默認容量:11(質數為宜)
-
put操作:首先進行索引計算 (key.hashCode() & 0x7FFFFFFF)% table.length;若在鏈表中找到了,則替換舊值,若未找到則繼續;當總元素個數超過 容量 * 加載因子 時,擴容為原來 2 倍并重新散列;將新元素加到鏈表頭部
-
對修改 Hashtable 內部共享數據的方法添加了 synchronized,保證線程安全
HashMap 與 Hashtable 區別
-
默認容量不同,HashMap是16,Hashtable是11。擴容不同
-
線程安全性:HashTable 安全,使用了synchronized同步
-
效率不同:HashTable 要慢,因為加鎖
?
Map是用來存儲key-value類型數據的,一個對在Map的接口定義中被定義為Entry,HashMap內部實現了Entry接口。HashMap內部維護一個Entry數組。 transient Entry[] table;
當put一個新元素的時候,根據key的hash值計算出對應的數組下標。數組的每個元素是一個鏈表的頭指針,用來存儲具有相同下標的Entry。
hashmap放入順序和輸出順序是不一致的!要想維持插入序,可使用LinkedHashMap
遍歷HashMap的四種方法:
public static void main(String[] args) {Map<String,String> map=new HashMap<String,String>();map.put("1", "value1");map.put("2", "value2");map.put("3", "value3");map.put("4", "value4");//第一種:普通使用,二次取值System.out.println("\n通過Map.keySet遍歷key和value:");? for(String key:map.keySet()){System.out.println("Key: "+key+" Value: "+map.get(key));}//第二種System.out.println("\n通過Map.entrySet使用iterator遍歷key和value: ");? Iterator map1it=map.entrySet().iterator();while(map1it.hasNext()){Map.Entry<String, String> entry=(Entry<String, String>) map1it.next();System.out.println("Key: "+entry.getKey()+" Value: "+entry.getValue());}//第三種:推薦,尤其是容量大時? System.out.println("\n通過Map.entrySet遍歷key和value");? for(Map.Entry<String, String> entry: map.entrySet()){System.out.println("Key: "+ entry.getKey()+ " Value: "+entry.getValue());}//第四種? System.out.println("\n通過Map.values()遍歷所有的value,但不能遍歷key");? for(String v:map.values()){System.out.println("The value is "+v);}}?
?
?
總結
以上是生活随笔為你收集整理的Java集合:HashMap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java中 String的反转
- 下一篇: java美元兑换,(Java实现) 美元