Java HashMap用法與實現


為了做題用Java語法替代C++map的常用語法,記錄一下,剖析原理以后再補上。

1.import java.util.HashMap;//導入;

2.HashMap<K, V> map=new HashMap<K, V>();//定義map,K和V是類,不允許基本類型;

3.void clear();//清空

4.put(K,V);//設置K鍵的值為V

5.V get(K);//獲取K鍵的值

6.boolean isEmpty();//判空

7.int size();//獲取map的大小

8.V remove(K);//刪除K鍵的值,返回的是V,可以不接收

9.boolean containsKey(K);//判斷是否有K鍵的值

10.boolean containsValue(V);//判斷是否有值是V

11.Object clone();//淺克隆,類型需要強轉;如HashMap<String , Integer> map2=(HashMap<String, Integer>) map.clone();

 

 


 

1.繼承與實現

繼承AbstractMap<K,V>,實現Map<K,V>, Cloneable, Serializable

2.基本屬性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默認初始化大小 16 
static final float DEFAULT_LOAD_FACTOR = 0.75f;     //負載因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {};         //初始化的默認數組
transient int size;     //HashMap中元素的數量
int threshold;          //判斷是否需要調整HashMap的容量

3.實現方式

jdk1.7是數組+鏈表,jdk1.8是數組+鏈表+紅黑樹。

二叉查找樹、平衡二叉樹、紅黑樹的概念

二叉查找樹,值唯一,在建樹的時候判斷插入的節點,如果比根節點小就插到左邊,比根節點大就插入到右邊,查找的時候通過判斷選擇正確的方向找下去,而不用遍歷一整棵樹,效率高。但如果插入值的時候是按順序插入的,一直加在左邊或者右邊形成一條鏈,查找和插入的效率就很慢,所以有了平衡二叉樹。

平衡二叉樹,左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。通過左旋右旋各種旋實現的,具體就不清楚了。這種旋轉就避免了二叉查找樹退化成鏈表導致查找效率過低的情況。但是嚴格控制高度的絕對值之差又導致在插入的時候頻繁地旋轉,浪費時間,所以有了紅黑樹。

紅黑樹,在每個節點加一個存儲為表示節點的顏色,非紅即黑。通過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍,在子樹高度差上沒有那么嚴格,旋轉的次數比較少。因此,紅黑樹是一種弱的平衡二叉樹。

4.了解一下hashCode

(一直以為hashCode是唯一的,錯得離譜啊)

Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作為散列值。對象在jvm上的內存位置是唯一的,但是不同對象的hashcode可能相同,它還要包括其他內容,再根據一定的算法去算出一個值,算出來的可能一樣,這就是哈希沖突。

5.哈希沖突

HashMap存的是對象,那就有一個哈希值,如果哈希值一樣,用鏈表解決哈希沖突,先定位到數組下標,再去鏈表里查找。

1.7是鏈表,頭插,我猜測頭插的理由是:新加入的值應該比舊的值更有可能用到,定位到數組節點時,在頭部能更快找到。不論頭插還是尾插,都需要把整條鏈表遍歷一遍,確定key在不在鏈表里。1.7版本中,產生哈希沖突時,遍歷一條鏈表查找對象,時間復雜度時O(n),隨着鏈表越來越長,查找的時間越來越大。

為了提高這個沖突的查找效率,1.8在鏈表長度超過8時,把鏈表轉變成紅黑樹,大大減少查找時間。為了防止鏈表或紅黑樹巨大,需要了解擴容這個概念。

6.擴容機制與負載因子

初始容器容量是16,負載因子默認0.75,最大容量230意思就是當前容量到達12(16*0.75=12)的時候,會觸發擴容機制。數據結構就是為了省時間省空間,擴容機制和負載因子的設定肯定也是為了效率。

(1)為什么負載因子是0.75?

如果負載因子太大,例如1時,只有當數組全部填充才會擴容,意味着會有大量的哈希沖突,紅黑樹變大變復雜,不利於添加查找。如果負載因子太小,例如0.5或者更低時,容量到達一半或者還不到一半的時候就開始擴容,看起來就有點浪費空間。負載因子的設定肯定是權衡了哈希沖突和容量大小。(個人推測,產生大量的對象放進容器,記錄哈希值和沖突情況,測試不同負載因子耗費的時間和空間,再用數據分析的方法多方面考慮,選一個最佳的負載因子作為默認值)如果想要空間換時間,減小負載因子,減少哈希沖突

(2)容器容量為什么是2的冪次方?

先了解一下put方法的流程:

  • 先檢查大小,如果需要擴容就先擴容;
  • 重新計算key的哈希值hash,定位到數組中的下標
  • 如果位置上沒有元素就直接插入,結束;
  • 如果有元素就用equal檢查key是否相同,如果相同就把新value替換舊value
  • key不同就往鏈表里繼續找,沒找到key就插入,找得到就替換舊value。

定位到數組中的下標,最簡單的方法就是對容量求模index=hash%n,然而源碼的計算方法是index=(n-1)&hash。

n是2的冪次方,n-1的二進制全是1,按位與和求模結果差不多,但是位運算是直接對內存數據進行操作,不需要轉成十進制,快

那么每次擴容也要是2的冪次方才能保證n-1的二進制全是1,如果不全是1計算出來的index不均勻。擴容總不會擴4倍8倍,所以是2倍。

擴容時原本位置也是有規律去變化的,不會丟失原來的索引。

例如一個對象的hash二進制是10111(23),在容量為16時,對15按位與計算得到的索引為

10111

&1111

=0111(7)

當容量擴大到32時,對31按位與計算得到的索引為

 10111

&11111

=10111(23)

23-7=16,16正好是擴容的大小。

7.線程不安全

在接近臨界點時,若此時兩個或者多個線程進行put操作,都會進行resize(擴容)和reHash(為key重新計算所在位置),而reHash在並發的情況下可能會形成鏈表環。總結來說就是在多線程環境下,使用HashMap進行put操作會引起死循環,導致CPU利用率接近100%,所以在並發情況下不能使用HashMap。為什么在並發執行put操作會引起死循環?是因為多線程會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不為空,就會產生死循環獲取Entry。jdk1.7的情況下,並發擴容時容易形成鏈表環,此情況在1.8時就好太多太多了。

因為在1.8中當鏈表長度達到閾值(默認長度為8)時,鏈表會被改成樹形(紅黑樹)結構。如果刪剩節點變成7個並不會退回鏈表,而是保持不變,刪剩6個時就會變回鏈表,7不變是緩沖,防止頻繁變換。

在JDK1.7中,當並發執行擴容操作時會造成環形鏈和數據丟失的情況。
在JDK1.8中,在並發執行put操作時會發生數據覆蓋的情況。

8.哈希碰撞拒絕服務攻擊

用哈希碰撞發起拒絕服務攻擊(DOS,Denial-Of-Service attack),常見的場景是攻擊者可以事先構造大量相同哈希值的數據,然后以JSON數據的形式發送給服務器,服務器端在將其構建成為Java對象過程中,通常以Hashtable或HashMap等形式存儲,哈希碰撞將導致哈希表發生嚴重退化,算法復雜度可能上升一個數據級,進而耗費大量CPU資源。

9.和兄弟HashTable的異同

(1)繼承和實現

HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都同時實現了Map、Cloneable(可復制)、Serializable(可序列化)這三個接口。存儲的內容是基於key-value的鍵值對映射,不能有重復的key,而且一個key只能映射一個value。HashSet底層就是基於HashMap實現的。

(2)key-value

HashMap支持key-value、null-value、key-null、null-null這4種方式,但HashTable只支持key-value。

HashMap不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷,因為使用get()的時候,當返回null時,你無法判斷到底是不存在這個key,還是這個key就是null,還是key存在但value是null。

(3)擴容

HashMap:默認初始容量是16,嚴格要求是2的冪次方,每次擴容到原來的2倍

HashTable:默認初始容量是11,不要求是2的冪次方,每次擴容到原來的2倍+1

(4)求索引index

HashMap求索引時用&運算,index=(n-1)&hash

HashTable求索引用模運算,index = (hash & 0x7FFFFFFF) % n

(5)線程安全方面

HashMap線程不安全,在並法包Java. util. concurrent的作用下它有一個對應的線程安全類ConcurrentHashMap

HashTable是線程安全的,它的一些方法加了synchronized。

10.了解一下LinkedHashMap

從Linked這個名字可以知道肯定和鏈表有關,它的數據結構附加了雙向鏈表,彌補HashMap無序的缺點。

HashMap在存入的時候通過&計算索引,這個索引不是有序的,所以在遍歷HashMap的時候,無法獲得插入時的順序。而LinkedHashMap把插入的節點用鏈表連接起來,通過鏈表來遍歷,可以獲得插入時的順序。(在不知道這個東西的情況下,要我獲取HashMap的插入順序的話,我會開兩個ArrayList或者LinkedList來記錄順序,並且一一對應key和value)。線程不安全。

11.了解一下HashSet

Map是映射,那就是key-value。Set是集合,無序不重復,存的只是key,不是兩個對象組成的鍵值對key-value。底層數據結構是HashMap,它存的對象放在key里。線程不安全。

12.了解一下HashTree

底層數據結構是裸的紅黑樹,保證元素有序,沒有比較器Comparator的情況按照key的自然排序,可自定義比較器。線程不安全。

 

參考:https://yuanrengu.com/2020/ba184259.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM