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

diamond_lin發表於2017-10-18

Map

我們都知道 Map 是鍵值對關係的集合,並且鍵唯一,鍵一對一對應值。

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

照慣例,我們來看類結構圖吧~~

都是一些行為控制的方法,用過 Map 集合的我們都熟悉這些方法,我就不做過多的贅述了,這裡我們重點來看看巢狀類 Map.Entry

Map.Entry

定義:對映項(鍵-值對)。Map.entrySet 方法返回對映的 collection 檢視,其中的元素屬於此類。獲得對映項引用的唯一 方法是通過此 collection 檢視的迭代器來實現。這些 Map.Entry 物件僅 在迭代期間有效;更確切地講,如果在迭代器返回項之後修改了底層對映,則某些對映項的行為是不確定的,除了通過 setValue 在對映項上執行操作之外。

這裡我們可以看到 Map 的泛型K,V也給 Map.Entry用了,然後根據定義,我們可以大膽的猜測這個 Entry 就是用來存放 K,V 等關鍵資訊的實體類。

我們來看看 Map.Entry 定義的方法

方法名 用途
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 2、3、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流的學習吧。

相關文章