1. 程式人生 > 實用技巧 >Java HashMap用法與實現 ZZ

Java HashMap用法與實現 ZZ

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

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

2.HashMap<K, V> map=newHashMap<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