LinkedHashMap
繼承自 HashMap
,在瞭解了 HashMap
之後,再來看看 LinkedHashMap
是如何維護它的順序的。
初始化
LinkedHashMap
的構造函數比 HashMap
的構造函數多維護了一個成員變量 accessOrder
。該成員變量如果爲 true ,則按訪問順序,否則按插入順序(默認爲 false)。
看下面這個例子:
Map<String, String> insertOrderMap = new LinkedHashMap<>();
for (int i = 0; i < 5; i++){
insertOrderMap.put(String.valueOf(i), String.valueOf(i));
}
assert insertOrderMap.keySet().toString().equals("[0, 1, 2, 3, 4]");
Map<String, String> accessOrderMap = new LinkedHashMap<>(16, 0.75f,true);
for (int i = 0; i < 5; i++){
accessOrderMap.put(String.valueOf(i), String.valueOf(i));
}
accessOrderMap.get(String.valueOf(3));
accessOrderMap.get(String.valueOf(1));
assert accessOrderMap.keySet().toString().equals("[0, 2, 4, 3, 1]");
斷言最終都會正確執行!在訪問順序模式下,調用 get
方法,改變了遍歷的順序。我們知道的是 LinkedHashMap
底層採用了雙向鏈表來維護順序,查看 get
方法實現:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
當 accessOrder
爲 true 時,會重新維護鏈表。
void afterNodeAccess(Node<K,V> e) { // 移動節點到最後
LinkedHashMap.Entry<K,V> last;
// 標記最後節點爲尾節點
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
// 當前節點爲頭節點,則將後繼節點設爲頭結點
if (b == null)
head = a;
// 當前節點不爲頭節點,則將前驅節點的後繼節點設爲當前節點的後繼節點
else
b.after = a;
// 當前節點不爲尾節點,則將後繼節點的前驅節點設爲當前節點的前驅節點
if (a != null)
a.before = b;
else
// 如果當前節點爲尾節點,則將最後的節點設置爲當前節點的前驅節點
last = b;
// 如果當前節點爲鏈表的第一個節點,則設置頭結點爲當前節點
if (last == null)
head = p;
else {
// 將當前節點放在最後的節點後
p.before = last;
last.after = p;
}
// 將當前節點作爲尾節點
tail = p;
++modCount;
}
}
上面的邏輯還是蠻多的,不過也是因爲要考慮的情況比較多,理解起來也不算太複雜。
存取操作
將 key 重新插入到 Map 中,插入順序不受影響。那如果是訪問順序模式呢?
查看 put
源碼,最終發現:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 更改鏈表順序
afterNodeAccess(e);
return oldValue;
}
上面的代碼是在 HashMap
的 putVal
方法的一部分。afterNodeAccess
由子類 LinkedHashMap
實現(模板方法模式)。僅僅只有在重新插入已經存在映射的 key 時,纔會調整訪問順序模式下的順序。
可能比較疑惑的是 在插入順序模式下,也沒有看見 LinkedHashMap
維護雙向鏈表啊 ?
其實不然,在 putVal
方法中有發現這樣一行代碼吧?
tab[i] = newNode(hash, key, value, null);
子類 LinkedHashMap
重寫了該方法,這種方式是真的漂亮!重寫後的方法如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
也就是說,LinkedHashMap
在 put 值的時候,其實,一直在維護鏈表,這裏需要注意。
刪除操作
LinkedHashMap
並沒有重寫 remove
方法,那麼 ,它是如何實現在刪除元素後進行雙向鏈表的處理的呢?查看 remove
方法,最終找到 afterNodeRemoval(node)
方法(模板方法模式),LinkedHashMap
實現了該方法 :
// 將當前節點從雙向鏈表當中剔除
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
// 如果當前節點爲頭節點,則重新設置頭節點
if (b == null)
head = a;
else
b.after = a;
// 如果當前節點爲尾節點,則重新設置尾節點
if (a == null)
tail = b;
else
a.before = b;
}
迭代器
重要的一步來了,在進行迭代器遍歷時,如何保證按照一定的順序(插入或者訪問)來遍歷呢?
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
// 從頭節點開始遍歷
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
迭代器的代碼比較容易理解,需要注意的是針對不同的集合視圖(keySet
,entrySet
和 values
),訪問不同的迭代器,僅僅是 next 方法中獲取節點的值(key
,node
和 value
)不同,所以在不同集合視圖下的不同迭代器,只要繼承 LinkedHashIterator
類,新增 next 方法,實現 Iterator
接口。
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
除了以上通過迭代器遍歷之外,還有一種方法,這是在 1.8 新加的 forEach
方法:
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
代碼實現來看,這種遍歷方法仍然是有序的!
在使用構造函數傳入的Map 爲LinkedHashMap
時, 重新構造的 LinkedHashMap
仍然維持傳入的 LinkedHashMap
的順序。
構建 LRU 緩存
這一點是從文檔中發現的,百度了一下 LRU
緩存:
LRU
是Least Recently Used的縮寫,即最近最少使用。
研究了下具體的實現,重載方法 removeEldestEntry(Map.Entry)
,實現移除最老的 Entry
策略即可。具體的原理是因爲 HashMap
在實現 putVal
方法時,調用了回調函數 afterNodeInsertion(evict)
,該函數由子類 LinkedHashMap
做了具體實現:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
正如上述代碼,我們只要實現 removeEldestEntry
方法,就能夠自定義移除最老的 Entry
策略了。
例:實現一個 容量爲 10 的 LRU
緩存:
static class LruCache<K,V> extends LinkedHashMap<K,V>{
private static int CAPACITY = 10;
public LruCache(){
super(16, 0.75f, true);
}
/**
* 緩存計數
*/
private AtomicInteger count = new AtomicInteger(0);
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if(count.getAndIncrement() >= CAPACITY ){
return true;
}
return false;
}
}
當實際存儲的元素數量大於 10 時,將會踢出最老的元素,爲了達到 LRU
,需要在訪問順序的模式下構造 LinkedHashMap
;這種實現最終會導致 int 類型溢出,可以使用如下這個類來代替:
public class AtomicPositiveInteger extends Number {
private static final long serialVersionUID = -3038533876489105940L;
private static final AtomicIntegerFieldUpdater<AtomicPositiveInteger> indexUpdater =
AtomicIntegerFieldUpdater.newUpdater(AtomicPositiveInteger.class, "index");
private volatile int index = 0;
public AtomicPositiveInteger() {
}
public final int getAndIncrement() {
// 溢出時做 與 運算,
return indexUpdater.getAndIncrement(this) & Integer.MAX_VALUE;
}
public final int get() {
return indexUpdater.get(this) & Integer.MAX_VALUE;
}
public final void set(int newValue) {
if (newValue < 0) {
throw new IllegalArgumentException("new value " + newValue + " < 0");
}
indexUpdater.set(this, newValue);
}
@Override
public byte byteValue() {
return (byte) get();
}
@Override
public short shortValue() {
return (short) get();
}
@Override
public int intValue() {
return get();
}
@Override
public long longValue() {
return (long) get();
}
@Override
public float floatValue() {
return (float) get();
}
@Override
public double doubleValue() {
return (double) get();
}
@Override
public String toString() {
return Integer.toString(get());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + get();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof AtomicPositiveInteger)) return false;
AtomicPositiveInteger other = (AtomicPositiveInteger) obj;
return intValue() == other.intValue();
}
}
上面這種方式,能夠保證不溢出(重新歸爲0),但是無法恢復到該有的容量大小,比如 10,如果要做到這步,應該需要同步了。
總結
從實際問題入手分析源碼,思路將會清晰很多,並就新的問題再次分析,直到分析完所有問題。這種類似遞歸般的學習方法還是很不錯的。
LinkedHashMap
的排序是由雙向鏈表維護的,鏈表的元素類型爲 LinkedHashMap.Entry<K,V>
,底層維護了一個 head 和 一個 tail 記錄這個雙向鏈表。
LinkedHashMap
的排序分爲:
- 插入排序:按照插入時的順序遍歷
- 訪問排序:在不進行任何訪問操作前(即未更改雙向鏈表),按照插入的順序遍歷;在進行訪問操作,例如 get 或者 put 一個已經存在的值 的時候,會調整雙向鏈表,最近訪問的會放在鏈表的末尾。
可以發現,HashMap
和 LinkedHashMap
的設計實現採用了大量模板方法模式,也就是父類在方法實現中調用未實現方法(或者說留給子類拓展的),子類實現方法,以此來改變行爲。
具體總結如下:
- 在插入數據時,
LinkedHashMap
重寫newNode
方法,來修改頭尾節點,維護雙向鏈表。 - 在存數據時(put 操作),
- 當存放已經存在的 key 值時,會通過實現
afterNodeAccess
方法來調整訪問順序模式下的雙向鏈表。 HashMap
預留afterNodeInsertion
回調方法,LinkedHashMap
實現了該方法, 插入數據時,判斷是否需要移除最老的元素(雙向鏈表頭)。這有點像個驚喜,我覺得LinkedHashMap
完全可以不做這個。
- 當存放已經存在的 key 值時,會通過實現
- 在刪除數據時,
LinkedHashMap
實現afterNodeRemoval
方法,來調整雙向鏈表。
推薦博文
手撕系列讓我很有食慾啊!!!
我與風來
認認真真學習,做思想的產出者,而不是文字的搬運工
錯誤之處,還望指出