第6節:Java基礎 - 三大集合(上)

第6節:Java基礎 - 三大集合(上)

本小節是Java基礎篇章的第四小節,主要介紹Java中的常用集合知識點,涉及到的內容包括Java中的三大集合的引出,以及HashMap,Hashtable和ConcurrentHashMap

三大集合接口的引出

Java中的集合,從上層接口上看分爲了兩類,Map和Collection。也就是說,我們平時接觸到的常用的集合,包括HashMap,ArrayList和HashSet等都直接或者間接的實現了這兩個接口之一。而Collection接口的子接口又包括了Set和List接口。這樣我們常見的Map,Set和List三大集合接口就出來了。接口類圖如下所示:

 

 

 

 

 

Map,List和Set都是Collection的子接口嗎?

Map是和Collection並列的集合上層接口,沒有繼承關係;List和Set是Collection的子接口。

(1)Java中常見的集合

Java中的常見集合可以概括如下。

  • Map接口和Collection接口是所有集合框架的父接口

  • Collection接口的子接口包括:Set接口和List接口

  • Map接口的實現類主要有:HashMap、TreeMap、Hashtable、LinkedHashMap、ConcurrentHashMap以及Properties等

  • Set接口的實現類主要有:HashSet、TreeSet、LinkedHashSet等

  • List接口的實現類主要有:ArrayListLinkedList、Stack以及Vector等

     

(2)HashMap和Hashtable的區別有哪些?

HashMap和Hashtable之間的區別可以總結如下。

  • HashMap沒有考慮同步,是線程不安全的;Hashtable使用了synchronized關鍵字,是線程安全的;

  • HashMap允許null作爲Key;Hashtable不允許null作爲Key,Hashtable的value也不可以爲null

 

解析:

這個算是針對HashMap的一個開胃小菜,既然說出了線程安全和不安全的區別,會接着考察線程安全的具體含義,如下所示:

HashMap是線程不安全的,可以舉一個例子

  • HashMap線程不安全主要是考慮到了多線程環境下進行擴容可能會出現HashMap死循環

  • Hashtable線程安全是由於其內部實現在put和remove等方法上使用synchronized進行了同步,所以對單個方法的使用是線程安全的。但是對多個方法進行複合操作時,線程安全性無法保證。 比如一個線程在進行get操作,一個線程在進行remove操作,往往會導致下標越界等異常。

 

既然說到了這裏,那麼我們來看看大家一直想說的Java集合快速失敗(fast-fail)機制是怎麼回事兒吧~

Java集合中的快速失敗(fast-fail)機制

快速失敗是Java集合的一種錯誤檢測機制,當多個線程對集合進行結構上的改變的操作時,有可能會產生fail-fast。

例如:

假設存在兩個線程(線程1、線程2),線程1通過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就可能會拋出 ConcurrentModificationException異常,從而產生fast-fail快速失敗。

那麼快速失敗機制底層是怎麼實現的呢?

迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。當迭代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否爲expectedModCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。JDK源碼中的判斷大概是這樣的:

 

 

 

 

(3)HashMap底層實現結構有了解嗎?

HashMap底層實現數據結構爲數組+鏈表的形式,JDK8及其以後的版本中使用了數組+鏈表+紅黑樹實現,解決了鏈表太長導致的查詢速度變慢的問題。大概結構如下圖所示:

 

 

 

HashMap的初始容量,加載因子,擴容增量是多少?

HashMap的初始容量16,加載因子爲0.75,擴容增量是原容量的1倍。如果HashMap的容量爲16,一次擴容後容量爲32。HashMap擴容是指元素個數(包括數組和鏈表+紅黑樹中)過了16*0.75=12之後開始擴容。

 

HashMap的長度爲什麼是2的冪次方?

  • 我們將一個鍵值對插入HashMap中,通過將Key的hash值與length-1進行&運算,實現了當前Key的定位,2的冪次方可以減少衝突(碰撞)的次數,提高HashMap查詢效率

  • 如果length爲2的冪次方,則length-1 轉化爲二進制必定是11111……的形式,在與h的二進制與操作效率會非常的快,而且空間不浪費

  • 如果length不是2的冪次方,比如length爲15,則length-1爲14,對應的二進制爲1110,在與h與操作,最後一位都爲0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的機率,減慢了查詢的效率!這樣就會造成空間的浪費。

 

接下來,我們來做一個簡單的總結:

總結:

也就是說2的N次冪有助於減少碰撞的機率,空間利用率比較大。這樣你就明白爲什麼第一次擴容會從16 ->32了吧?總不會再說32+1=33或者其餘答案了吧?至於加載因子,如果設置太小不利於空間利用,設置太大則會導致碰撞增多,降低了查詢效率,所以設置了0.75。

上邊介紹了HashMap在存儲空間不足的時候會進行擴容操作。那麼,我們接着來看HashMap中的存儲和擴容等相關知識點吧。

 

HasMap的存儲和獲取原理:

調用put()方法傳遞鍵和值來存儲時,先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象,也就是找到了該元素應該被存儲的桶中(數組)。當兩個鍵的hashCode值相同時,bucket位置發生了衝突,也就是發生了Hash衝突,這個時候,會在每一個bucket後邊接上一個鏈表(JDK8及以後的版本中還會加上紅黑樹)來解決,將新存儲的鍵值對放在表頭(也就是bucket中)。

當調用get方法獲取存儲的值時,首先根據鍵的hashCode找到對應的bucket,然後根據equals方法來在鏈表和紅黑樹中找到對應的值。

HasMap的擴容步驟:

HashMap裏面默認的負載因子大小爲0.75,也就是說,當Map中的元素個數(包括數組,鏈表和紅黑樹中)超過了16*0.75=12之後開始擴容。將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因爲它調用hash方法找到新的bucket位置。

但是,需要注意的是在多線程環境下,HashMap擴容可能會導致死循環

前面我們介紹了在HashMap存儲的時候,會發生Hash衝突,那麼我們一起來看Hash衝突的解決辦法吧。

 

解決Hash衝突的方法有哪些?

  • 拉鍊法 (HashMap使用的方法)

  • 線性探測再散列法

  • 二次探測再散列法

  • 僞隨機探測再散列法

 

哪些類適合作爲HashMap的鍵?

String和Interger這樣的包裝類很適合做爲HashMap的鍵,因爲他們是final類型的類,而且重寫了equals和hashCode方法,避免了鍵值對改寫,有效提高HashMap性能。爲了計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashCode的話,那麼就不能從HashMap中找到你想要的對象。

擴展知識點:

在高級的算法中,還有一個一致性Hash算法,有能力和精力的同學可以去研究下“一致性Hash算法”,有所瞭解一致性Hash算法對於面試是一個很好的加分點。

(4)ConcurrentHashMap和Hashtable的區別?

ConcurrentHashMap結合了HashMap和Hashtable二者的優勢。HashMap沒有考慮同步,Hashtable考慮了同步的問題。但是Hashtable在每次同步執行時都要鎖住整個結構。

ConcurrentHashMap鎖的方式是稍微細粒度的,ConcurrentHashMap將hash表分爲16個桶(默認值),諸如get,put,remove等常用操作只鎖上當前需要用到的桶。

ConcurrentHashMap的具體實現方式(分段鎖):

  • 該類包含兩個靜態內部類MapEntry和Segment,前者用來封裝映射表的鍵值對,後者用來充當鎖的

     

     

  • Segment是一種可重入的鎖ReentrantLock,每個Segment守護一個HashEntry數組裏得元素,當對HashEntry數組的數據進行修改時,必須首先獲得對應的Segment鎖。

     

     

    解析: ConcurrentHashMap與Hashtable以及HashMap的比較是一個絕對高頻的考察點,我們必須熟練掌握ConcurrentHashMap分段鎖的實現方式。在實際的開發中,我們在單線程環境下可以使用HashMap,多線程環境下可以使用ConcurrentHashMap,至於Hashtable已經不被推薦使用了(也就是說Hashtable只存在於面試題目中了)。

    總結:

    本小節中,我們交流學習了Java基礎中的三大集合,重點闡述了HashMap相關的知識點。這裏鄭重提示,本小節所涉及到的內容幾乎是面試中的必現考察點。有能力的同學,最好是打開JDK的源碼,好好研究HashMap以及ConcurentHashMap的實現方式。當然如果你遇到問題,可以在評論區留言,我們可以一起探討學習,一起進步。

    限於作者水平,文章中難免會有不妥之處。大家在學習過程中遇到我沒有表達清楚或者表述有誤的地方,歡迎隨時在文章下邊指出,我會及時關注,隨時改正。另外,大家有任何話題都可以在下邊留言,我們一起交流探討。

    附圖:

    集合的類圖:

    HashMap的類圖結構:

    ConcurrentHashMap的類圖結構:

     

     Hashtable的類圖結構:

     

     

發佈了675 篇原創文章 · 獲贊 30 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章