Java 基礎(七)集合源碼解析 Map

Map

咱們都知道 Map 是鍵值對關係的集合,而且鍵惟一,鍵一對一對應值。java

關於 Map 的定義,大概就這些吧,API 文檔的定義也是醬紫。程序員

照慣例,咱們來看類結構圖吧~~面試

都是一些行爲控制的方法,用過 Map 集合的咱們都熟悉這些方法,我就不作過多的贅述了,這裏咱們重點來看看嵌套類 Map.Entry編程

Map.Entry

定義:映射項(鍵-值對)。Map.entrySet 方法返回映射的 collection 視圖,其中的元素屬於此類。得到映射項引用的惟一 方法是經過此 collection 視圖的迭代器來實現。這些 Map.Entry 對象僅 在迭代期間有效;更確切地講,若是在迭代器返回項以後修改了底層映射,則某些映射項的行爲是不肯定的,除了經過 setValue 在映射項上執行操做以外。後端

這裏咱們能夠看到 Map 的泛型K,V也給 Map.Entry用了,而後根據定義,咱們能夠大膽的猜想這個 Entry 就是用來存放 K,V 等關鍵信息的實體類。數組

咱們來看看 Map.Entry 定義的方法bash

方法名 用途
equals(Object o) 比較指定對象與此項的相等性
getKey() 返回與此項對應的鍵
getValue() 返回與此項對應的值
hashCode() 返回此映射的哈希碼值
setValue() 用指定的值替換與此項對應的值

這裏的5個方法,除了 hashCode 以外,都能顧名思義。那麼問題來了,咱們這裏的實體類爲何要引入 hashCode 的概念呢~~這裏咱們要先學習一種數據結構---散列表。數據結構

Map 的抽象實現類 AbstractMap

此類提供 Map 接口的骨幹實現,以最大限度地減小實現此接口所需的工做。併發

要實現不可修改的映射,編程人員只需擴展此類並提供 entrySet 方法的實現便可,該方法將返回映射的映射關係 set 視圖。一般,返回的 set 將依次在 AbstractSet 上實現。此 set 不支持 add 或 remove 方法,其迭代器也不支持 remove 方法。框架

要實現可修改的映射,編程人員必須另外重寫此類的 put 方法(不然將拋出 UnsupportedOperationException),entrySet().iterator() 返回的迭代器也必須另外實現其 remove 方法。

按照 Map 接口規範中的建議,編程人員一般應該提供一個 void(無參數)構造方法和 map 構造方法。

此類中每一個非抽象方法的文檔詳細描述了其實現。若是要實現的映射容許更有效的實現,則能夠重寫全部這些方法。

HashMap

這個是最正宗的基於哈希表的 Map 接口實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。

HashMap 的實例有兩個參數影響其性能:初始容量 和加載因子。容量 是哈希表中bucketIndex(桶)的數量,初始容量只是哈希表在建立時的容量。加載因子 是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。

一般,默認加載因子 (0.75) 在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數 HashMap 類的操做中,包括 get 和 put 操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小 rehash 操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash 操做。

注意,此實現不是同步的。若是多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須 保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關係的任何操做;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這通常經過對天然封裝該映射的對象進行同步操做來完成。若是不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來「包裝」該映射。最好在建立時完成這一操做,以防止對映射進行意外的非同步訪問,以下所示:

Map m = Collections.synchronizedMap(new HashMap(...));複製代碼

咱們先了解如下三個名詞,若是還不懂的話也沒事,咱們一塊兒手擼一個 HashMap就是了。

散列表

散列表,又名哈希表(Hash table),是根據關鍵碼值(Key value)而進行訪問的數據結構,也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表

給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能獲得包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。

提及來可能有點抽象,我給你們舉個栗子吧~~
對於一個具備 n 個元素的數組,咱們須要找到其中某一個值的時間複雜度是 O(n)。如今咱們使用散列表來實現,要獲取一個元素「vaule」,咱們能夠經過該元素的 Key 值「key」來獲取「value」在數組中存放的位置,而後直接獲取到咱們須要的元素,則獲取的時間複雜度是 O(1)。那麼問題是,怎麼經過「key」來獲取呢,上面的定義有說到。把關鍵詞「key」代入到哈希函數裏面計算,計算的結果就是「value」存放的位置。key 是個泛型,要使不一樣類型的數據都能帶入到哈希函數裏面進行計算,這裏咱們用的是對象的 hashCode 值,hashCode 值的定義這裏不過多贅述,你們記得 hashCode 是 Object 的方法便可。

看到這裏若是還不明白的話,那我就只能講本身的理解了:就是經過哈希函數計算一個 Keyhash 值,獲得一個bucketIndex(能夠理解爲數組角標),把 Value 存放到這個bucketIndex對應的位置。下次再取這個 Vaule 的時候只需把 key 的 hash代入到哈希函數裏面進行計算獲得 Value 的位置便可。

鍵惟一

上一篇咱們分析 HashSet 源碼怎麼實現集合元素不重複的時候,我挖了一個坑,如今來把這個坑給填上吧。

要比較兩個元素是否相等,這個在 java 裏面彷佛是一個比較簡單的問題,可是要把==,equals 和 hashcode 牽扯進來,好像又有點講不清楚。

  • 首先咱們來區分「==」和 equals 的區別

"=="在比較基本數據類型的時候比較的是值是否相等,在比較對象變量的時候比較的是兩個變量是否指向同一個對象。equals Object 的方法,用於比較兩個對象內容是否相等。默認是用==作比較,若是重寫則單獨處理。

  • hashCode 的約定
    • 在 Java 應用程序執行期間,在對同一對象屢次調用 hashCode 方法時,必須一致地返回相同的整數,前提是將對象進行 equals 比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另外一次執行,該整數無需保持一致。
    • 若是根據 equals(Object) 方法,兩個對象是相等的,那麼對這兩個對象中的每一個對象調用 hashCode 方法都必須生成相同的整數結果。
    • 若是根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼對這兩個對象中的任一對象上調用 hashCode 方法不 要求必定生成不一樣的整數結果。可是,程序員應該意識到,爲不相等的對象生成不一樣整數結果能夠提升哈希表的性能。

好,看完這些,咱們能夠知道,若是兩個對象相等,那麼hashCode 的值必然相等;若是兩個對象不相等,hashCode 的值也有可能相等,只是兩個不相等的對象 hashCode 值相等的話,哈希表的性能會降低。

好了,看到這裏咱們能夠根據上面的結論,先獲取是否有 hash 相同的 key,若是有,再執行==和 equals 操做比較,若是沒有。。。那就算鍵惟一。

鍵衝突

剛剛咱們在保證鍵惟一的時候有一個這樣的問題不知道你們注意到了沒,就是不一樣的對象,其 hashCode的值是有可能相等的,那麼當兩個不一樣的 key 存入 map,而正好其key 的 hash 值相等,那該怎麼辦?

這個實際上是屬於散列表的問題,可是散列表牽扯到的東西太多,因此剛剛我沒有講。可是咱們剛剛在保持鍵惟一的時候又碰到了這個問題,那麼咱們來簡單講一下吧。

剛剛咱們在講散列表數據結構的時候已經說了,其實散列表的數據存儲就是一個數組,哈希函數根據 key計算出來的 hash 值就是 value 的存放位置,而 value 則是一個Map.Entry 的具體實現類,Java 的 HashMap 在這裏用了「拉鍊法」來鍵衝突。什麼是拉鍊法呢?就是讓 value 變成一個鏈表,添加一個指向下一個元素的引用。

也就是說,map 的數據存儲其實就是一個數組,當咱們插入一組 K,V 的時候,用 K 的 hashcode 通過 hash 函數計算得出 V 存放的位置 bucketIndex,而後若是 數組[bucketIndex]沒有元素,則建立 Entry 賦值給 數組[bucketIndex]。若是有1或多個元素(多個元素以鏈表的形式),則遍歷比較各組元素的key 是否和插入 key 相等,若是相等則覆蓋 value,不然new 一個 Entry 插入到表頭。

HashTable

繼承自 Dictionary 類,實現了 Map 接口。

Dictionary 類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類。每一個鍵和每一個值都是一個對象。在任何一個 Dictionary 對象中,每一個鍵至多與一個值相關聯。給定一個 Dictionary 和一個鍵,就能夠查找所關聯的元素。任何非 null 對象均可以用做鍵或值。

基本上不會再使用的類,K、V 都不能爲 null,支持同步算是惟一的優勢了吧。可是 Java 推薦咱們使用 Collections.synchronized* 對各類集合進行了同步的封裝,因此基本廢棄。

TreeMap

咱們先來看看類註釋說明~

A Red-Black tree based NavigableMap implementation. The map is sorted according to the Comparable natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used.

TreeMap 是 Map 接口基於紅黑樹的實現,鍵值對是有序的。TreeMap 的排序方式基於Key的Comparable 接口的比較,或者在構造方法中傳入一個Comparator.

This implementation provides guaranteed log(n) time cost for the {@code containsKey}, {@code get}, {@code put} and {@code remove} operations. Algorithms are adaptations of those in Cormen, Leiserson, and Rivest's Introduction to Algorithms.

TreeMap提供時間複雜度爲log(n)的containsKey,get,put ,remove操做。

和 HashMap 的區別?

  • TreeMap的元素是有序的,HashMap的元素是無序的。
  • TreeMap的鍵值不能爲 null。

大體的特性就這些吧,關鍵就是掌握紅黑樹。

說回來,TreeMap 就是Java 的紅黑樹實現啊~~

紅黑樹

紅黑樹是一種自平衡二叉查找樹,是一種比較特別的數據結構。

紅黑樹和 AVL 樹相似,都是在進行插入和刪除操做保持二叉查找樹的平衡,從而活得較高的查找性能。

紅黑樹雖然負責,可是它最壞的運行時間也是很是良好的,而且在實踐中是高效的,它能夠在 O(lon n)時間內作查找、插入和刪除,這裏的 n 指樹種元素的數目。

額、若是對樹以及二叉樹不瞭解的同窗能夠跳過這一段。

五大特性

  • 節點是紅色或者黑色
  • 根是黑色
  • 全部的葉子都是黑色的(葉子是 NIL 節點)
  • 每一個紅色節點的兩個子節點都是黑色(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點)
  • 從任一節點到其每一個葉子的全部簡單路徑 都包含相同數目的黑色節點。

咱們來對着下面這張圖來理解一下這五大特徵。

1.節點都是紅色或者黑色——沒毛病
2.根節點是黑色——沒毛病
3.全部的葉子節點都是黑色——能夠理解成最外層的節點都會有一個 NIL 的黑色節點,實際數據結構中就是 null
4.每一個紅色節點的兩個子節點都是黑色——注意不能反過來,這是爲了保證不會有兩個連續的紅色節點。
5.保證黑色節點數相同,也就是保證了從根節點到葉子節點最短的路徑*2>=最長的路徑

樹的旋轉
當咱們在堆紅黑樹進行插入和刪除等操做時,對樹作了修改,那麼可能會違背紅黑樹的性質。

爲了保持紅黑樹的性質,咱們能夠經過對樹進行旋轉操做,即修改樹種某些節點的顏色及指針結構,已達到對紅黑樹進行插入、刪除節點等操做時,紅黑樹依然能保持它特有的性質。

說白了,就是增刪節點的時候會破壞樹的性質,因此經過旋轉來保持。

怎樣旋轉?爲何旋轉能夠保持紅黑樹性質?
這個~~旋轉是一門玄學,通常看運氣才能正確的作出旋轉操做。
至於爲何旋轉能夠保持樹的性質,這個……你能夠暫且把這個旋轉理解成是一個「定理」吧。

定理:是通過受邏輯限制的證實爲真的陳述

好了,別糾結了,你記住旋轉能夠保持樹的性質就好了。

樹的插入
樹的插入很簡單,只能插入到葉子節點上(若是插入的 key 在樹上已存在,則覆蓋 Value),根據左節點小又節點大的性質,找到對應的葉節點插入便可,注意插入的節點默認只能是紅色。若是插入的葉子節點的父節點是紅色,則違背了特性4,這時候就須要經過旋轉來穩定樹的性質。

怎樣旋轉

旋轉分了左右旋轉,左旋操做有以下幾步
1.把跟節點 a 的右孩子 c 做爲根節點
2.把舊的根節點 a 做爲新的根節點 c 的左孩子
3.把新的跟節點 c 的左孩子做爲 a 節點的右孩子

右旋就簡單了,把上面步驟的左右對調就好了。

好了,旋轉就這樣,很簡單,可是必定要用紙和筆在紙上畫一畫才能掌握哦~~

何時旋轉?作什麼旋轉?

mmp,這個問題我操蛋了很久很久,我怎麼知道怎樣旋轉。去搜別人寫的 blog 。。。。。

而後搜到了各類圖解,看了幾篇仍是半懂不懂的,索性本身去分析源碼。。。。

在 Treemap 裏面 put 插入了一個節點以後有個fixAfterInsertion()操做。看名字咱們就知道是插入後修復。

源碼就不帶着你們一塊兒讀了,後面我手擼 RBTree的時候會一步一步講解。

我用文字表述一下fixAfterInsertion(x)裏面的邏輯。

首先把 x 節點設爲紅色,這裏的 x 節點就是新插入的節點。

當 x 節點不爲空,x 節點不爲跟節點,x 的父節點是紅色的時候,while 如下操做。

敲黑板,這裏邏輯比較複雜,裏面各類 if else 邏輯,注意了!!!

爲了不有人說我嚇唬小朋友,我仍是貼一下代碼吧~~

while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            TreeMapEntry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }複製代碼

邏輯仍是蠻複雜的,其實總結清楚了以後,就只有如下三種狀況。

  • case 1:當前節點的父節點是紅色,叔叔節點(這個名詞能理解吧,不能理解我也沒辦法了)也是紅色。
    1. 將父節點設爲黑色
    2. 將叔叔節點設爲黑色
    3. 將祖父節點設爲紅色
    4. 將祖父節點設爲當前節點,從新判斷 case
  • case 2:當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是父節點的右孩子
    1. 若是當前節點是右孩子,將當前節點指向父節點,並自身左旋
    2. 設置父節點黑色,爺爺節點紅色
    3. 以爺爺節點爲支點右旋,從新判斷 case
  • case 3:當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是父節點的左孩子
    1. 若是當前節點是左孩子,將當前節點指向父節點,並自身右旋
    2. 設置父節點黑色,爺爺節點紅色
    3. 以爺爺節點爲支點左旋,從新判斷 case
  • case 4:啥?不是隻有3種狀況麼?當前節點的父節點是黑色
    1. 不用修復,樹穩定。

樹的刪除操做
這他喵的也是一段很鬧騰的代碼。

刪除就是根據 key 找到對應的節點,並執行刪除操做,刪除以後爲了保證樹的穩定性,須要根據節點的一些屬性進行調整。

這裏主要處理兩個問題:

  • 如何刪除
  • 刪除後如何調整

刪除節點分三種狀況
1.沒有兒子的節點
直接刪除便可
2.有一個兒子的節點,把父節點的兒子指針指向本身的兒子,再刪除便可(就像鏈表中刪除一個元素)
3.有兩個兒子的節點,首先找到右兒子最左邊的葉節點或者左兒子最右邊的葉子節點a,再把 a 賦值給本身,而後刪除 a 節點。

這個比較容易,應該能理解吧。。。。。。理解不了本身去畫畫圖~

刪除後如何修復,咱們再來根據剛剛刪除節點的三種狀況分析

1.1 沒有兒子的節點,且當前節點爲紅色。直接刪除便可,不影響樹的性質
1.2 沒有兒子的節點,且當前節點爲黑色。執行刪除後修復操做,傳參是被刪除的節點
2.1 有一個兒子節點,且當前節點爲紅色。直接刪除便可,不影響樹的性質
2.2 有一個兒子節點,且當前節點爲黑色。執行刪除後修復操做,傳參是被刪除節點的子節點
3.1 有兩個兒子節點,且找到的後備葉子節點是紅色。直接刪除便可,不影響樹的性質
3.2 有兩個兒子節點,且找到的後備葉子節點是黑色。執行刪除後修復操做,傳參是被刪除的葉子節點

好了,刪除的邏輯咱們看完了,反正就是傳一個指定的節點 x 到fixAfterDeletion(x)到這個方法裏面執行修復操做。

接下來,咱們就來看看怎樣修復吧~
已知修復是根據傳參的節點來判斷的,而後裏面也有不少 if else 等語句,邏輯和插入修復差很少,也很複雜。這裏我先給你們總結一下方法裏面的邏輯 case

  • case 1:x 的兄弟節點是紅色
    1. 將 x 的兄弟節點設爲黑色
    2. 將 x 的父節點設爲紅色
    3. 以 x 的父節點爲支點進行左旋
    4. 後續邏輯爲 case 二、三、4隨機一種
  • case 2:x 的兄弟節點是黑色,且兄弟節點的兩個孩子都是黑色
    1. 將 x 的兄弟節點設爲紅色
    2. x 指向 x 的父節點,繼續 while 循環
  • case 3:x 的兄弟節點是黑色,且兄弟的左孩子是紅色、右孩子是黑色
    1. 將 x 的兄弟節點設爲紅色
    2. 將 x 的兄弟節點左孩子設爲黑色
    3. 以 x 的兄弟節點爲支點進行右旋
    4. 執行 case 4
  • case 4:x 的兄弟節點是黑色,且兄弟的左孩子是黑色、右孩子是紅色
    1. 將 x 的父節點顏色賦值給 x 的兄弟節點顏色
    2. 將 x 的父節點設爲黑色
    3. 將 x 的右孩子設爲黑色
    4. 以 x 的父節點爲支點進行左旋
  • case 5:若是 x 是左孩子,以上4個 case 的操做均沒毛病,若是 x 的右孩子,以上左右取反。
private void fixAfterDeletion(Entry<K,V> x) {  
    // 刪除節點須要一直迭代,知道 直到 x 不是根節點,且 x 的顏色是黑色  
    while (x != root && colorOf(x) == BLACK) {  
        if (x == leftOf(parentOf(x))) {      //若X節點爲左節點  
            //獲取其兄弟節點  
            Entry<K,V> sib = rightOf(parentOf(x));  

            /* 
             * 若是兄弟節點爲紅色----(case 1) 
             */  
            if (colorOf(sib) == RED) {       
                setColor(sib, BLACK);       
                setColor(parentOf(x), RED);    
                rotateLeft(parentOf(x));  
                sib = rightOf(parentOf(x));  
            }  

            /* 
             * 若兄弟節點的兩個子節點都爲黑色----(case 2) 
             */  
            if (colorOf(leftOf(sib))  == BLACK &&  
                colorOf(rightOf(sib)) == BLACK) {  
                setColor(sib, RED);  
                x = parentOf(x);  
            }   
            else {  
                /* 
                 * 若是兄弟節點只有右子樹爲黑色----(case 3)  
                 * 這時狀況會轉變爲case 4 
                 */  
                if (colorOf(rightOf(sib)) == BLACK) {  
                    setColor(leftOf(sib), BLACK);  
                    setColor(sib, RED);  
                    rotateRight(sib);  
                    sib = rightOf(parentOf(x));  
                }  
                /* 
                 *case 4 
                 */  
                setColor(sib, colorOf(parentOf(x)));  
                setColor(parentOf(x), BLACK);  
                setColor(rightOf(sib), BLACK);  
                rotateLeft(parentOf(x));  
                x = root;  
            }  
        }   

        /** 
         * case 5
         */  
        else {  
            Entry<K,V> sib = leftOf(parentOf(x));  

            if (colorOf(sib) == RED) {  
                setColor(sib, BLACK);  
                setColor(parentOf(x), RED);  
                rotateRight(parentOf(x));  
                sib = leftOf(parentOf(x));  
            }  

            if (colorOf(rightOf(sib)) == BLACK &&  
                colorOf(leftOf(sib)) == BLACK) {  
                setColor(sib, RED);  
                x = parentOf(x);  
            } else {  
                if (colorOf(leftOf(sib)) == BLACK) {  
                    setColor(rightOf(sib), BLACK);  
                    setColor(sib, RED);  
                    rotateLeft(sib);  
                    sib = leftOf(parentOf(x));  
                }  
                setColor(sib, colorOf(parentOf(x)));  
                setColor(parentOf(x), BLACK);  
                setColor(leftOf(sib), BLACK);  
                rotateRight(parentOf(x));  
                x = root;  
            }  
        }  
    }  

    setColor(x, BLACK);  
    }  複製代碼

WeakHashMap

可能不少同窗沒用過這個類,沒吃過豬肉,應該見過豬跑吧,根據名字猜想,大體能知道這是一個跟弱引用有關係的 HashMap。
咱們來看看官方文檔的定義

以弱鍵 實現的基於哈希表的 Map。在 WeakHashMap 中,當某個鍵再也不正常使用時,將自動移除其條目。更精確地說,對於一個給定的鍵,其映射的存在並不阻止垃圾回收器對該鍵的丟棄,這就使該鍵成爲可終止的,被終止,而後被回收。丟棄某個鍵時,其條目從映射中有效地移除,所以,該類的行爲與其餘的 Map 實現有所不一樣。

嗯~~大體就是告訴咱們,key 除了被 HashMap 引用以外沒有任何引用,就會自動刪掉這個 key 以及 value。

弱引用的概念:弱引用是用來描述非必需對象的,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前,當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

大體咱們也知道了這是怎麼回事,就是控制 key 的外部引用,能夠控制 HashMap 裏面保存數據的存留,在大量數據的讀取刪除的時候,咱們能夠考慮使用 HashMap。

接下來咱們經過一段代碼來學習怎麼控制弱引用。

Map<String, String> weak = new WeakHashMap<String, String>();
weak.put(new String("1"), "1");
weak.put(new String("2"), "2");
weak.put(new String("3"), "3");
weak.put(new String("4"), "4");
weak.put(new String("5"), "5");
weak.put(new String("6"), "6");
Log.e("weak1:",weak.size()+"");//6
Runtime.getRuntime().gc();  //手動觸發 Full GC
try {
     Thread.sleep(50); //個人測試中發現必須sleep一下才能看到不同的結果
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
Log.e("weak2:",weak.size()+"");//0

Map<String, String> weak2 = new WeakHashMap<String, String>();
weak2.put("1", "1");
weak2.put("2", "2");
weak2.put("3", "3");
weak2.put("4", "4");
weak2.put("5", "5");
weak2.put("6", "6");
Log.e("weak3:",weak2.size()+"");//6
Runtime.getRuntime().gc();
try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    e.printStackTrace();
}
Log.e("weak4:",weak2.size()+"");//6複製代碼

打印結果在代碼後面的註釋裏面,從這裏咱們能夠看到。weak裏面的 key 值只有weak 對其持有引用,因此在調用 gc 以後,weak的 size 就變成了0.這裏有兩點須要注意,一是調用 gc 不能用 System.gc(),而要用Runtime.getRuntime().gc()。二是要分得清new String("1")和「1」的區別。

接下來,咱們就來看看key 弱引用是如何關聯的。

查看源碼咱們能看到,幾乎全部的方法都直接或者間接的調用了expungeStaleEntries()方法,咱們來看看這個方法。

/**
 * Expunges stale entries from the table.
 */
private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, table.length);

            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            while (p != null) {
                Entry<K,V> next = p.next;
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    // Must not null out e.next;
                    // stale entries may be in use by a HashIterator
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}複製代碼

方法名已經方法註釋都告訴了咱們,這個方法是在清除 tab 裏面過時的元素。可是我找遍了整個 WeakHashMap 的源碼,都沒有找到任何 queue.add()的操做,mmp,這特麼幾個意思。最後,細心的我在 WeakHashMap 的 put 方法裏面找到了這樣之後代碼 tab[i] = new Entry<>(k, value, queue, h, e);
很少說了,直接去看Entry的構造方法。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
int hash;
Entry<K,V> next;

/**
 * Creates new entry.
 */
Entry(Object key, V value,
      ReferenceQueue<Object> queue,
      int hash, Entry<K,V> next) {
    super(key, queue);
    this.value = value;
    this.hash  = hash;
    this.next  = next;
}
...複製代碼

咱們能夠看到Entry 繼承自WeakReference,而後把key 和queue 傳到了WeakReference 的構造方法中,而後調用了父類Reference 的方法。

好了,到這裏就不用太糾結了,就是在Reference 裏面作的操做。大體的流程是這樣的:JVM計算對象key 的可達性後,發現沒有該key 對象的引用,那麼就會把該對象關聯的Entry添加到pending中,因此每次垃圾回收時發現弱引用對象沒有被引用時,就會將該對象放入待清除隊列中,最後由應用程序來完成清除,WeakHashMap中就負責由方法expungeStaleEntries()來完成清除。

其實這裏關於Reference 我本身也沒有弄得很清楚,下次找個時間單獨學Reference 機制。

ConCurrentMap

併發集合類,之後在併發的時候再看吧。
挺重要的一個冷門知識點,Android 幾乎用不上高併發,剛剛問了 Java 後端的同窗,他們也說沒用過。。。。是由於我沒去過大廠的緣由麼~~~
聽說大廠面試常常會問這個知識點。
一樣遺漏的還有 BlockingQueue.

IdentityHashMap

一個 Key 值能夠重複的 map.

此類利用哈希表實現 Map 接口,比較鍵(和值)時使用引用相等性代替對象相等性。換句話說,在 IdentityHashMap 中,當且僅當 (k1= =k2) 時,才認爲兩個鍵 k1 和 k2 相等(在正常 Map 實現(如 HashMap)中,當且僅當知足下列條件時才認爲兩個鍵 k1 和 k2 相等:(k1= =null ? k2= =null : e1.equals(e2))

也就是說,只有當 兩個 key 指向同一引用的時候,纔會執行覆蓋操做。

用途?舉個例子,jvm 中全部的對象都是獨一無二的,哪怕兩個對象是同一個 class 的對象,並且兩個對象的數據徹底相同,對於 jvm 來講,他們也是徹底不相同的,若是要用一個 map 來記錄這樣jvm 中的對象,就須要用到 IdentityHashMap。

具體我也沒用過~~😀

結束語

集合篇到這裏就差很少結束了,總的來講,只分析了框架,但並非知道了框架設計,捋清了實現思路,就必定能手擼出來,要想深刻掌握,還得本身跟着思路去手擼一遍。接下來再花一天的時間手擼 ArrayList、HashMap、TreeMap,就正式開始 I/O流的學習吧。

相關文章
相關標籤/搜索