1. 程式人生 > >轉:【Java集合源碼剖析】LinkedHashmap源碼剖析

轉:【Java集合源碼剖析】LinkedHashmap源碼剖析

知識庫 csdn val ride 初始 views 特性 read details

轉載請註明出處:http://blog.csdn.net/ns_code/article/details/37867985

前言:有網友建議分析下LinkedHashMap的源碼,於是花了一晚上時間研究了下,分享出此文(這個系列的最後一篇博文了),希望大家相互學習。LinkedHashMap的源碼理解起來也不難(當然,要建立在對HashMap源碼有較好理解的基礎上)。

LinkedHashMap簡介

LinkedHashMap是HashMap的子類,與HashMap有著同樣的存儲結構,但它加入了一個雙向鏈表的頭結點,將所有put到LinkedHashmap的節點一一串成了一個雙向循環鏈表,因此它保留了節點插入的順序,可以使節點的輸出順序與輸入順序相同。

LinkedHashMap可以用來實現LRU算法(這會在下面的源碼中進行分析)。

LinkedHashMap同樣是非線程安全的,只在單線程環境下使用。

LinkedHashMap源碼剖析

LinkedHashMap源碼如下(加入了詳細的註釋):

[java] view plain copy
  1. package java.util;
  2. import java.io.*;
  3. public class LinkedHashMap<K,V>
  4. extends HashMap<K,V>
  5. implements Map<K,V>
  6. {
  7. private static final long serialVersionUID = 3801124242820219131L;
  8. //雙向循環鏈表的頭結點,整個LinkedHa只喲shMap中只有一個header,
  9. //它將哈希表中所有的Entry貫穿起來,header中不保存key-value對,只保存前後節點的引用
  10. private transient Entry<K,V> header;
  11. //雙向鏈表中元素排序規則的標誌位。
  12. //accessOrder為false,表示按插入順序排序
  13. //accessOrder為true,表示按訪問順序排序
  14. private final boolean accessOrder;
  15. //調用HashMap的構造方法來構造底層的數組
  16. public LinkedHashMap(int initialCapacity, float loadFactor) {
  17. super(initialCapacity, loadFactor);
  18. accessOrder = false; //鏈表中的元素默認按照插入順序排序
  19. }
  20. //加載因子取默認的0.75f
  21. public LinkedHashMap(int initialCapacity) {
  22. super(initialCapacity);
  23. accessOrder = false;
  24. }
  25. //加載因子取默認的0.75f,容量取默認的16
  26. public LinkedHashMap() {
  27. super();
  28. accessOrder = false;
  29. }
  30. //含有子Map的構造方法,同樣調用HashMap的對應的構造方法
  31. public LinkedHashMap(Map<? extends K, ? extends V> m) {
  32. super(m);
  33. accessOrder = false;
  34. }
  35. //該構造方法可以指定鏈表中的元素排序的規則
  36. public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
  37. super(initialCapacity, loadFactor);
  38. this.accessOrder = accessOrder;
  39. }
  40. //覆寫父類的init()方法(HashMap中的init方法為空),
  41. //該方法在父類的構造方法和Clone、readObject中在插入元素前被調用,
  42. //初始化一個空的雙向循環鏈表,頭結點中不保存數據,頭結點的下一個節點才開始保存數據。
  43. void init() {
  44. header = new Entry<K,V>(-1, null, null, null);
  45. header.before = header.after = header;
  46. }
  47. //覆寫HashMap中的transfer方法,它在父類的resize方法中被調用,
  48. //擴容後,將key-value對重新映射到新的newTable中
  49. //覆寫該方法的目的是為了提高復制的效率,
  50. //這裏充分利用雙向循環鏈表的特點進行叠代,不用對底層的數組進行for循環。
  51. void transfer(HashMap.Entry[] newTable) {
  52. int newCapacity = newTable.length;
  53. for (Entry<K,V> e = header.after; e != header; e = e.after) {
  54. int index = indexFor(e.hash, newCapacity);
  55. e.next = newTable[index];
  56. newTable[index] = e;
  57. }
  58. }
  59. //覆寫HashMap中的containsValue方法,
  60. //覆寫該方法的目的同樣是為了提高查詢的效率,
  61. //利用雙向循環鏈表的特點進行查詢,少了對數組的外層for循環
  62. public boolean containsValue(Object value) {
  63. // Overridden to take advantage of faster iterator
  64. if (value==null) {
  65. for (Entry e = header.after; e != header; e = e.after)
  66. if (e.value==null)
  67. return true;
  68. } else {
  69. for (Entry e = header.after; e != header; e = e.after)
  70. if (value.equals(e.value))
  71. return true;
  72. }
  73. return false;
  74. }
  75. //覆寫HashMap中的get方法,通過getEntry方法獲取Entry對象。
  76. //註意這裏的recordAccess方法,
  77. //如果鏈表中元素的排序規則是按照插入的先後順序排序的話,該方法什麽也不做,
  78. //如果鏈表中元素的排序規則是按照訪問的先後順序排序的話,則將e移到鏈表的末尾處。
  79. public V get(Object key) {
  80. Entry<K,V> e = (Entry<K,V>)getEntry(key);
  81. if (e == null)
  82. return null;
  83. e.recordAccess(this);
  84. return e.value;
  85. }
  86. //清空HashMap,並將雙向鏈表還原為只有頭結點的空鏈表
  87. public void clear() {
  88. super.clear();
  89. header.before = header.after = header;
  90. }
  91. //Enty的數據結構,多了兩個指向前後節點的引用
  92. private static class Entry<K,V> extends HashMap.Entry<K,V> {
  93. // These fields comprise the doubly linked list used for iteration.
  94. Entry<K,V> before, after;
  95. //調用父類的構造方法
  96. Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
  97. super(hash, key, value, next);
  98. }
  99. //雙向循環鏈表中,刪除當前的Entry
  100. private void remove() {
  101. before.after = after;
  102. after.before = before;
  103. }
  104. //雙向循環立鏈表中,將當前的Entry插入到existingEntry的前面
  105. private void addBefore(Entry<K,V> existingEntry) {
  106. after = existingEntry;
  107. before = existingEntry.before;
  108. before.after = this;
  109. after.before = this;
  110. }
  111. //覆寫HashMap中的recordAccess方法(HashMap中該方法為空),
  112. //當調用父類的put方法,在發現插入的key已經存在時,會調用該方法,
  113. //調用LinkedHashmap覆寫的get方法時,也會調用到該方法,
  114. //該方法提供了LRU算法的實現,它將最近使用的Entry放到雙向循環鏈表的尾部,
  115. //accessOrder為true時,get方法會調用recordAccess方法
  116. //put方法在覆蓋key-value對時也會調用recordAccess方法
  117. //它們導致Entry最近使用,因此將其移到雙向鏈表的末尾
  118. void recordAccess(HashMap<K,V> m) {
  119. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  120. //如果鏈表中元素按照訪問順序排序,則將當前訪問的Entry移到雙向循環鏈表的尾部,
  121. //如果是按照插入的先後順序排序,則不做任何事情。
  122. if (lm.accessOrder) {
  123. lm.modCount++;
  124. //移除當前訪問的Entry
  125. remove();
  126. //將當前訪問的Entry插入到鏈表的尾部
  127. addBefore(lm.header);
  128. }
  129. }
  130. void recordRemoval(HashMap<K,V> m) {
  131. remove();
  132. }
  133. }
  134. //叠代器
  135. private abstract class LinkedHashIterator<T> implements Iterator<T> {
  136. Entry<K,V> nextEntry = header.after;
  137. Entry<K,V> lastReturned = null;
  138. /**
  139. * The modCount value that the iterator believes that the backing
  140. * List should have. If this expectation is violated, the iterator
  141. * has detected concurrent modification.
  142. */
  143. int expectedModCount = modCount;
  144. public boolean hasNext() {
  145. return nextEntry != header;
  146. }
  147. public void remove() {
  148. if (lastReturned == null)
  149. throw new IllegalStateException();
  150. if (modCount != expectedModCount)
  151. throw new ConcurrentModificationException();
  152. LinkedHashMap.this.remove(lastReturned.key);
  153. lastReturned = null;
  154. expectedModCount = modCount;
  155. }
  156. //從head的下一個節點開始叠代
  157. Entry<K,V> nextEntry() {
  158. if (modCount != expectedModCount)
  159. throw new ConcurrentModificationException();
  160. if (nextEntry == header)
  161. throw new NoSuchElementException();
  162. Entry<K,V> e = lastReturned = nextEntry;
  163. nextEntry = e.after;
  164. return e;
  165. }
  166. }
  167. //key叠代器
  168. private class KeyIterator extends LinkedHashIterator<K> {
  169. public K next() { return nextEntry().getKey(); }
  170. }
  171. //value叠代器
  172. private class ValueIterator extends LinkedHashIterator<V> {
  173. public V next() { return nextEntry().value; }
  174. }
  175. //Entry叠代器
  176. private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
  177. public Map.Entry<K,V> next() { return nextEntry(); }
  178. }
  179. // These Overrides alter the behavior of superclass view iterator() methods
  180. Iterator<K> newKeyIterator() { return new KeyIterator(); }
  181. Iterator<V> newValueIterator() { return new ValueIterator(); }
  182. Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); }
  183. //覆寫HashMap中的addEntry方法,LinkedHashmap並沒有覆寫HashMap中的put方法,
  184. //而是覆寫了put方法所調用的addEntry方法和recordAccess方法,
  185. //put方法在插入的key已存在的情況下,會調用recordAccess方法,
  186. //在插入的key不存在的情況下,要調用addEntry插入新的Entry
  187. void addEntry(int hash, K key, V value, int bucketIndex) {
  188. //創建新的Entry,並插入到LinkedHashMap中
  189. createEntry(hash, key, value, bucketIndex);
  190. //雙向鏈表的第一個有效節點(header後的那個節點)為近期最少使用的節點
  191. Entry<K,V> eldest = header.after;
  192. //如果有必要,則刪除掉該近期最少使用的節點,
  193. //這要看對removeEldestEntry的覆寫,由於默認為false,因此默認是不做任何處理的。
  194. if (removeEldestEntry(eldest)) {
  195. removeEntryForKey(eldest.key);
  196. } else {
  197. //擴容到原來的2倍
  198. if (size >= threshold)
  199. resize(2 * table.length);
  200. }
  201. }
  202. void createEntry(int hash, K key, V value, int bucketIndex) {
  203. //創建新的Entry,並將其插入到數組對應槽的單鏈表的頭結點處,這點與HashMap中相同
  204. HashMap.Entry<K,V> old = table[bucketIndex];
  205. Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
  206. table[bucketIndex] = e;
  207. //每次插入Entry時,都將其移到雙向鏈表的尾部,
  208. //這便會按照Entry插入LinkedHashMap的先後順序來叠代元素,
  209. //同時,新put進來的Entry是最近訪問的Entry,把其放在鏈表末尾 ,符合LRU算法的實現
  210. e.addBefore(header);
  211. size++;
  212. }
  213. //該方法是用來被覆寫的,一般如果用LinkedHashmap實現LRU算法,就要覆寫該方法,
  214. //比如可以將該方法覆寫為如果設定的內存已滿,則返回true,這樣當再次向LinkedHashMap中put
  215. //Entry時,在調用的addEntry方法中便會將近期最少使用的節點刪除掉(header後的那個節點)。
  216. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  217. return false;
  218. }
  219. }

幾點總結

關於LinkedHashMap的源碼,給出以下幾點比較重要的總結:

1、從源碼中可以看出,LinkedHashMap中加入了一個head頭結點,將所有插入到該LinkedHashMap中的Entry按照插入的先後順序依次加入到以head為頭結點的雙向循環鏈表的尾部。

技術分享技術分享

實際上就是HashMap和LinkedList兩個集合類的存儲結構的結合。在LinkedHashMapMap中,所有put進來的Entry都保存在如第一個圖所示的哈希表中,但它又額外定義了一個以head為頭結點的空的雙向循環鏈表,每次put進來Entry,除了將其保存到對哈希表中對應的位置上外,還要將其插入到雙向循環鏈表的尾部。

2、LinkedHashMap由於繼承自HashMap,因此它具有HashMap的所有特性,同樣允許key和value為null。

3、註意源碼中的accessOrder標誌位,當它false時,表示雙向鏈表中的元素按照Entry插入LinkedHashMap到中的先後順序排序,即每次put到LinkedHashMap中的Entry都放在雙向鏈表的尾部,這樣遍歷雙向鏈表時,Entry的輸出順序便和插入的順序一致,這也是默認的雙向鏈表的存儲順序;當它為true時,表示雙向鏈表中的元素按照訪問的先後順序排列,可以看到,雖然Entry插入鏈表的順序依然是按照其put到LinkedHashMap中的順序,但put和get方法均有調用recordAccess方法(put方法在key相同,覆蓋原有的Entry的情況下調用recordAccess方法),該方法判斷accessOrder是否為true,如果是,則將當前訪問的Entry(put進來的Entry或get出來的Entry)移到雙向鏈表的尾部(key不相同時,put新Entry時,會調用addEntry,它會調用creatEntry,該方法同樣將新插入的元素放入到雙向鏈表的尾部,既符合插入的先後順序,又符合訪問的先後順序,因為這時該Entry也被訪問了),否則,什麽也不做。

4、註意構造方法,前四個構造方法都將accessOrder設為false,說明默認是按照插入順序排序的,而第五個構造方法可以自定義傳入的accessOrder的值,因此可以指定雙向循環鏈表中元素的排序規則,一般要用LinkedHashMap實現LRU算法,就要用該構造方法,將accessOrder置為true。

5、LinkedHashMap並沒有覆寫HashMap中的put方法,而是覆寫了put方法中調用的addEntry方法和recordAccess方法,我們回過頭來再看下HashMap的put方法:

[java] view plain copy
  1. // 將“key-value”添加到HashMap中
  2. public V put(K key, V value) {
  3. // 若“key為null”,則將該鍵值對添加到table[0]中。
  4. if (key == null)
  5. return putForNullKey(value);
  6. // 若“key不為null”,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。
  7. int hash = hash(key.hashCode());
  8. int i = indexFor(hash, table.length);
  9. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  10. Object k;
  11. // 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
  12. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  13. V oldValue = e.value;
  14. e.value = value;
  15. e.recordAccess(this);
  16. return oldValue;
  17. }
  18. }
  19. // 若“該key”對應的鍵值對不存在,則將“key-value”添加到table中
  20. modCount++;
  21. //將key-value添加到table[i]處
  22. addEntry(hash, key, value, i);
  23. return null;
  24. }

當要put進來的Entry的key在哈希表中已經在存在時,會調用recordAccess方法,當該key不存在時,則會調用addEntry方法將新的Entry插入到對應槽的單鏈表的頭部。

我們先來看recordAccess方法:

[java] view plain copy
  1. //覆寫HashMap中的recordAccess方法(HashMap中該方法為空),
  2. //當調用父類的put方法,在發現插入的key已經存在時,會調用該方法,
  3. //調用LinkedHashmap覆寫的get方法時,也會調用到該方法,
  4. //該方法提供了LRU算法的實現,它將最近使用的Entry放到雙向循環鏈表的尾部,
  5. //accessOrder為true時,get方法會調用recordAccess方法
  6. //put方法在覆蓋key-value對時也會調用recordAccess方法
  7. //它們導致Entry最近使用,因此將其移到雙向鏈表的末尾
  8. void recordAccess(HashMap<K,V> m) {
  9. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  10. //如果鏈表中元素按照訪問順序排序,則將當前訪問的Entry移到雙向循環鏈表的尾部,
  11. //如果是按照插入的先後順序排序,則不做任何事情。
  12. if (lm.accessOrder) {
  13. lm.modCount++;
  14. //移除當前訪問的Entry
  15. remove();
  16. //將當前訪問的Entry插入到鏈表的尾部
  17. addBefore(lm.header);
  18. }
  19. }

該方法會判斷accessOrder是否為true,如果為true,它會將當前訪問的Entry(在這裏指put進來的Entry)移動到雙向循環鏈表的尾部,從而實現雙向鏈表中的元素按照訪問順序來排序(最近訪問的Entry放到鏈表的最後,這樣多次下來,前面就是最近沒有被訪問的元素,在實現、LRU算法時,當雙向鏈表中的節點數達到最大值時,將前面的元素刪去即可,因為前面的元素是最近最少使用的),否則什麽也不做。
再來看addEntry方法:

[java] view plain copy
  1. //覆寫HashMap中的addEntry方法,LinkedHashmap並沒有覆寫HashMap中的put方法,
  2. //而是覆寫了put方法所調用的addEntry方法和recordAccess方法,
  3. //put方法在插入的key已存在的情況下,會調用recordAccess方法,
  4. //在插入的key不存在的情況下,要調用addEntry插入新的Entry
  5. void addEntry(int hash, K key, V value, int bucketIndex) {
  6. //創建新的Entry,並插入到LinkedHashMap中
  7. createEntry(hash, key, value, bucketIndex);
  8. //雙向鏈表的第一個有效節點(header後的那個節點)為近期最少使用的節點
  9. Entry<K,V> eldest = header.after;
  10. //如果有必要,則刪除掉該近期最少使用的節點,
  11. //這要看對removeEldestEntry的覆寫,由於默認為false,因此默認是不做任何處理的。
  12. if (removeEldestEntry(eldest)) {
  13. removeEntryForKey(eldest.key);
  14. } else {
  15. //擴容到原來的2倍
  16. if (size >= threshold)
  17. resize(2 * table.length);
  18. }
  19. }
  20. void createEntry(int hash, K key, V value, int bucketIndex) {
  21. //創建新的Entry,並將其插入到數組對應槽的單鏈表的頭結點處,這點與HashMap中相同
  22. HashMap.Entry<K,V> old = table[bucketIndex];
  23. Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
  24. table[bucketIndex] = e;
  25. //每次插入Entry時,都將其移到雙向鏈表的尾部,
  26. //這便會按照Entry插入LinkedHashMap的先後順序來叠代元素,
  27. //同時,新put進來的Entry是最近訪問的Entry,把其放在鏈表末尾 ,符合LRU算法的實現
  28. e.addBefore(header);
  29. size++;
  30. }

同樣是將新的Entry插入到table中對應槽所對應單鏈表的頭結點中,但可以看出,在createEntry中,同樣把新put進來的Entry插入到了雙向鏈表的尾部,從插入順序的層面來說,新的Entry插入到雙向鏈表的尾部,可以實現按照插入的先後順序來叠代Entry,而從訪問順序的層面來說,新put進來的Entry又是最近訪問的Entry,也應該將其放在雙向鏈表的尾部。

上面還有個removeEldestEntry方法,該方法如下:

[java] view plain copy
  1. //該方法是用來被覆寫的,一般如果用LinkedHashmap實現LRU算法,就要覆寫該方法,
  2. //比如可以將該方法覆寫為如果設定的內存已滿,則返回true,這樣當再次向LinkedHashMap中put
  3. //Entry時,在調用的addEntry方法中便會將近期最少使用的節點刪除掉(header後的那個節點)。
  4. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  5. return false;
  6. }
  7. }

該方法默認返回false,我們一般在用LinkedHashMap實現LRU算法時,要覆寫該方法,一般的實現是,當設定的內存(這裏指節點個數)達到最大值時,返回true,這樣put新的Entry(該Entry的key在哈希表中沒有已經存在)時,就會調用removeEntryForKey方法,將最近最少使用的節點刪除(head後面的那個節點,實際上是最近沒有使用)。
6、LinkedHashMap覆寫了HashMap的get方法:

[java] view plain copy
  1. //覆寫HashMap中的get方法,通過getEntry方法獲取Entry對象。
  2. //註意這裏的recordAccess方法,
  3. //如果鏈表中元素的排序規則是按照插入的先後順序排序的話,該方法什麽也不做,
  4. //如果鏈表中元素的排序規則是按照訪問的先後順序排序的話,則將e移到鏈表的末尾處。
  5. public V get(Object key) {
  6. Entry<K,V> e = (Entry<K,V>)getEntry(key);
  7. if (e == null)
  8. return null;
  9. e.recordAccess(this);
  10. return e.value;
  11. }

先取得Entry,如果不為null,一樣調用recordAccess方法,上面已經說得很清楚,這裏不在多解釋了。 7、最後說說LinkedHashMap是如何實現LRU的。首先,當accessOrder為true時,才會開啟按訪問順序排序的模式,才能用來實現LRU算法。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成為最近訪問的Entry,因此便把該Entry加入到了雙向鏈表的末尾(get方法通過調用recordAccess方法來實現,put方法在覆蓋已有key的情況下,也是通過調用recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向鏈表的後面,多次操作後,雙向鏈表前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head後面的那個Entry)便是最近最少使用的Entry。

轉:【Java集合源碼剖析】LinkedHashmap源碼剖析