Map
我們都知道 Map 是鍵值對關系的集合,并且鍵唯一,鍵一對一對應值。
關于 Map 的定義,大概就這些吧,API 文檔的定義也是醬紫。
照慣例,我們來看類結構圖吧~~
都是一些行為控制的方法,用過 Map 集合的我們都熟悉這些方法,我就不做過多的贅述了,這里我們重點來看看嵌套類 Map.Entry
Map.Entry<K,V>
定義:映射項(鍵-值對)。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 的方法即可。
看到這里如果還不明白的話,那我就只能講自己的理解了:就是通過哈希函數計算一個 Key 的 hash 值,得到一個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 <em>Introduction to Algorithms</em>.
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:當前節點的父節點是紅色,叔叔節點(這個名詞能理解吧,不能理解我也沒辦法了)也是紅色。
- 將父節點設為黑色
- 將叔叔節點設為黑色
- 將祖父節點設為紅色
- 將祖父節點設為當前節點, case 結束,可能會重新走第二輪 while
- case 2:當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是父節點的右孩子
- 將父節點作為新的當前節點
- 以新的當前節點作為支點左旋
- case 3:當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是父節點的左孩子
- 將父節點設為黑色
- 將祖父節點設為紅色
- 以祖父節點為支點進行右旋
- case 4:啥?不是只有3種情況么?當前節點的父節點是黑色
- 不用修復,樹穩定。
樹的刪除操作
這他喵的也是一段很鬧騰的代碼。
刪除就是根據 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 的兄弟節點是紅色
- 將 x 的兄弟節點設為黑色
- 將 x 的父節點設為紅色
- 以 x 的父節點為支點進行左旋
- 后續邏輯為 case 2、3、4隨機一種
- case 2:x 的兄弟節點是黑色,且兄弟節點的兩個孩子都是黑色
- 將 x 的兄弟節點設為紅色
- x 指向 x 的父節點,繼續 while 循環
- case 3:x 的兄弟節點是黑色,且兄弟的左孩子是紅色、右孩子是黑色
- 將 x 的兄弟節點設為紅色
- 將 x 的兄弟節點左孩子設為黑色
- 以 x 的兄弟節點為支點進行右旋
- 執行 case 4
- case 4:x 的兄弟節點是黑色,且兄弟的左孩子是黑色、右孩子是紅色
- 將 x 的父節點顏色賦值給 x 的兄弟節點顏色
- 將 x 的父節點設為黑色
- 將 x 的右孩子設為黑色
- 以 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<K,V>添加到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流的學習吧。