Java基础 HashMap实现原理及方法
1、什么是HashMap?
????? ? HashMap通常提起他,我們想到的就是鍵值對方式存儲(key-value型式),可以接收null鍵值和null值。基于Map接口的非同步實現(也就是線程不安全),并不保證映射的順序,特別不保證這個順序恒久不變。
????????? ? 下圖是HashMap的源碼,可以看到它繼承自AbstractMap,實現Map,Cloneable,Serializable接口。其一些默認值都一一列出:
????????? ? 其中,modCount這個屬性值是記錄HashMap內部結構發生變化(指的是內部結構,相同的key,put()一個value這種覆蓋原來的值不屬于結構變化)的次數,主要用于迭代的快速失敗。
????????? ? threshold來判斷HashMap的最大容量,threshold = (int)(capacity * loadFactor);
????????? ? loadFactor負載因子:散列表的實際元素數目/散列表的容量。
??????????其衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高(裝東西越滿),使用鏈表法?的散列 表空間利用越充分,查找時間則變長效率降低;越小則結論與越大相反。
???????????Entry為HashMap的靜態內部類,從上面可以看出Entry為承載key-value的值,并且默認table為Entry數組。下面看一下Entry的源碼:
????? ? put時如果key傳入為null,那么value一定會為null,無論value輸入什么.并且map.put()/get()的返回值是value的類型,并且當存入空的時候為hashmap中數組第一位。下面是對應的小栗子:
1、
Map<String, String> map2 = new HashMap<String,String>();
String r2 = map2.put(null, null);
System.out.println("r2 "+r2);
String r3 = map2.get(null);
System.out.println("r3 "+r3);
String r4 = map2.put(null, "32");
System.out.println("r4 "+r4);
????????????輸出 結果:r2 null
??????????????????????????????r3 null
??????????????????????????????r4 null
2、
3、從此可以看出hashmap的hash方法如果key為空則默認0,否則key取hashcode異或運算h無符號右移16位
2、HashMap的數據結構是什么樣的呢?
????? ? jdk的1.8之前的版本和1.8及之后的版本HashMap的數據結構出現了一點變化。我們這里主要介紹1.8之前的數據結構。主要是數組+鏈表的結構,上面給出的屬性源碼也可以看出HashMap是這樣的數據結構(table數組,Entry類為一個鏈表的節點來存儲Key-value),也叫拉鏈法(鏈表數組)。
????? ? 在1.8之后HashMap的數據結構出現了改變,變成數組+鏈表+紅黑樹,默認值增加了樹的相關默認值,Entry類改成Node但是類里面內容沒有改變。
????????????并且增添了TreeNode節點,繼承自LinkedHashMap,LinkedHashMap則繼承自HashMap。下面是TreeNode內容:
?????????????紅黑樹是一種自平衡二叉查找樹。它的統計性能要好于平衡二叉樹(AVL樹)。這種樹結構從根節點開始,左子節點小于它,右子節點大于它。每個節點都符合這個特性,所以易于查找,是一種很好的數據結構。但是它有一個問題,就是容易偏向某一側,這樣就像一個鏈表結構了,失去了樹結構的優點,查找時間會變壞。(這里就詳細講解這個結構了以后會開一個數據結構的專欄)
????? ?閱讀源碼可以看出樹比鏈表占了更多的空間。8后,決定如何使用這兩種數據結構呢??
? ??????? ?1、如果一個內部表的索引超過8個節點,鏈表會轉化為紅黑樹?
???????????2、 如果內部表的索引少于6個節點,樹會變回鏈表
? ? ? ? ? 當鏈表元素個數大于等于8時,鏈表轉換成樹結構;若桶中鏈表元素個數小于等于6時,樹結構還原成鏈表。因為紅黑樹的平均查找長度是log(n),長度為8的時候,平均查找長度為3,如果繼續使用鏈表,平均查找長度為8/2=4,這才有轉換為樹的必要。鏈表長度如果是小于等于6,6/2=3,雖然速度也很快的,但是轉化為樹結構和生成樹的時間并不會太短。還有選擇6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小于8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。
3、HashMap的工作原理
????????? ? 它的原理就是Hashing原理。在上一部分我們講到了HashMap的數組結構,每添加(put())或是獲取(get())都是向每個數組位(array[i])對應為一個bucket里的Entry中添加鍵值對。而通過hashCode()得出一個hash值來算出bucket(水桶)的位置來進行存取(這里的運算都是位運算)。
4、HashMap的存取
? 1、存入主要是put()方法:? ??????????????
????????? ? 可以從上面源碼看出在put時,根據hash值來確定存放數組的索引位置。如果該位置上已經存放了Entry那么,那么將會以鏈表的形式存放鍵值對,原來的Entry放在next進行鏈接,及新加入的放鏈頭,原來的放鏈尾。如果該位置上沒有Entry則直接存放到對應的數組索引的位置上。
?2、取出get()方法:
????? ? 從源碼上可以看出get()也是先算取key的hash值,然后找到hash值對應的索引,看是否有鏈表,若有則看是否和鏈表中Entry的key是否相等,相等則equals()取value值。實際上就是將key-value看成一個整體Entry,來用hashCode()來查找hash值,如果有鏈表就判斷鏈表,沒有鏈表就直接取對應的數組索引。
?3、擴容resize()(rehash)方法:
5、HashMap的一些問題思考
1、什么時候會出現擴容呢?
????
????? ? 源碼上addEntry是size大于threshold時開始調用resize()方法。也就是HashMap中,數組元素超過了數組閥值的時候就重新擴容,以降低實際的負載因子。(threshold>capacity*loadfactor)默認的的負載因子 0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize后的HashMap 容量是原容量的兩倍。
2、HashMap容量一定為2的冪呢?
????????? ? 首先,我們希望元素存放的更均勻,最理想的效果是每個bucket中存放一個Entry(key-value)。這樣查詢的時候效率高,不需要遍歷鏈表,也不需要equals去比較鏈表中的key的內容。而且,這樣空間利用率最大,時間復雜度最優。因為HashMap的底層數組的長度為2^n次方,不同的key算得的index的相同幾率較小,數組上分布就比較均勻,也就是碰撞幾率小,相對查詢時效率會高。
????????????假設:數組長度為32時,即為2的n次方時,2n-1得到的二進制數的每個位上的值都為1,這使得在低位上&時,得到的和原hash的低位相同,加之hash(int h)方法對key的hashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值才會被放到數組中的同一個位置上形成鏈表。
3、Hash沖突(碰撞)及解決Hash沖突的方法?????????
??????????? ?hash沖突就是當hash值相同,此時他們確定的索引位置相同,這時他們的key如果不相同,則為hash沖突。
????????? ? 解決hash沖突的辦法:
????????????????? ? 通常有:1、開放定址法 (基本思想是通過一個探測算法,當某個槽位已經被占據的情況下據需查找下一個可以使用的槽位)。?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 2、再哈希法(基本思想是同事構造多個哈希函數,當函數1沖突時,再用下一個方法計算,直到沖突不再產生)。這種方法不已長生聚焦,但增加計算時間。? ??????????????????????????????????????? ?? ??
?
??????????????????????????????????3、鏈地址法(基本思想是將相同的hash值的對象組成一個成為同義詞鏈的單鏈表,并將單鏈表的頭指針存在哈希表的這個hash值中)。適用于經常插入和刪除。
??????????????????????????????????4、建立公共溢出區(將哈希表分為基本表和溢出表兩部分,凡是發生沖突的都放在溢出表中)。
????????????? ? Java.until.HashMap就是采用鏈表法的方式。當發生碰撞,對象將會存儲在LinkedList的下一結點中。HashMap在每個LinkedList節點中存儲鍵值對對象。
4、重新調整HashMap大小存在的問題?
????? ????HashMap數組擴容后很消耗性能:原數組中的數據必須重新計算其出現在新數組中的位置,并存儲進去。
?????????在多線程下,HashMap擴容后可能會產生條件競爭(race condition)。如果兩個線程都發現HashMap需要重新調整大小,他們會同時試著調整大小,在調整大小的過程中,存儲在LinkedList中的元素次序會反過來,因為移動到新的bucket位置的時候Hashmap并不會將元素放在LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭產生,就會出現死循環。
5、Fail-First機制?
???java.util.HashMap 不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了 hashmap,那么將拋出 ConcurrentModificationException,這就是所謂 fail-fast 策略。?
? ? ? ?在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map,則會拋出異常,下圖是源代碼:?
? 解決辦法:
????????1、在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,這樣就可以解決。但是不推薦,因為增刪造成的同步鎖可能會阻塞遍歷操作。? ? ? ? ??
????????2、使用CopyOnWriteArrayList來替換ArrayList。推薦使用該方案。
6、為什么String, Interger這樣的wrapper類適合作為鍵?
????????? ?因為他們由final修飾,使用不可變類就能保證hashCode是不變的,而且重寫了equals和hashcode方法,避免了鍵值對改寫。如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,提高HashMap性能。
????????
總結
以上是生活随笔為你收集整理的Java基础 HashMap实现原理及方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java基础 ArrayList和Li
- 下一篇: Java Set集合详解及Set与Lis