JDK原始碼分析(四)——LinkedHashMap
LinkedHashMap概述
JDK對LinkedHashMap的介紹:
Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return true immediately prior to the invocation.)
大意是:LinkedHashMap是通過雜湊表和連結串列來實現Map介面,它通過維護一個連結串列來保證對雜湊表迭代時的有序性,而這個有序是指鍵值對插入的順序。另外,當向雜湊表中重複插入某個鍵的時候,不會影響到原來的有序性。
繼承結構
可以看到LinkedHashMap直接繼承了HashMap,複用了HashMap的很多方法,比如put、resize等方法,LinkedHashMap是在HashMap的基礎上實現了自己的功能:有序插入。
資料結構
可以看到,LinkedHashMap資料結構相比較於HashMap來說,新增了雙向指標,其中before指向節點的前繼節點,after指向節點的後繼節點,從而將所有的節點串聯在一起形成一個雙向連結串列。
內部欄位及構造方法
內部欄位
//雙連結串列頭節點
transient Entry<K,V> head;
//雙連結串列尾節點
transient Entry<K,V> tail;
//accessOrder為true則表示按照基於訪問的順序來排列,意思就是最近使用的entry,
//放在連結串列的最末尾,為false表示按照基於插入的順序來排列,後插入的放在連結串列末尾,不指定預設為false
final boolean accessOrder;
static class Entry<K,V> extends HashMap.Node<K,V> {
//雙連結串列前繼、後繼節點
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
構造方法
可以看到,LinkedHashMap呼叫了父類的構造方法,而且預設的accessOrder是false,至於是怎麼通過accessOrder控制元素順序,我們將在方法講到。
//指定accessOrder的值
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//按照預設值初始化
public LinkedHashMap() {
super();
accessOrder = false;
}
//指定初始化時的容量
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//指定初始化時的容量,和擴容的載入因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
儲存元素
LinkedHashMap並沒有重寫父類的put方法,所以增加元素呼叫的是父類方法,具體來說是putVal方法,下面是HashMap的putVal方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
tab[i] = newNode(hash, key, value, null);
...
afterNodeAccess(e);
...
afterNodeInsertion(evict);
...
LinkedHashMap重寫了newNode和回撥方法afterNodeAccess、afterNodeInsertion:
//在構建新節點時,構建的是LinkedHashMap.Entry 不再是Node.
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
Entry<K,V> p =
new Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
//將新增的節點,連線在連結串列的尾部
private void linkNodeLast(Entry<K,V> p) {
Entry<K,V> last = tail;
tail = p;
//若集合是空的
if (last == null)
head = p;
//新節點插到連結串列頂部
else {
p.before = last;
last.after = p;
}
}
//僅僅在accessOrder為true時進行,把當前訪問的元素移動到連結串列尾部
void afterNodeAccess(Node<K,V> e) { // move node to last
Entry<K,V> last;
//當accessOrder的值為true,且e不是尾節點
if (accessOrder && (last = tail) != e) {
//將e賦值臨時節點p, b是e的前一個節點, a是e的後一個節點
Entry<K,V> p =
(Entry<K,V>)e, b = p.before, a = p.after;
//設定p的後一個節點為null,因為執行後p在連結串列末尾,after肯定為null
p.after = null;
//p的前一個節點不存在,p就是頭節點,那麼把p放到最後,a就是頭節點
if (b == null)
head = a;
//p的前一個節點存在,p放到最後,b的後一個節點指向a
else
b.after = a;
//p的後一個節點存在,p放到最後,a的前一個節點指向a
if (a != null)
a.before = b;
//p的後一個節點不存在
else
last = b;
//只有一個p節點
if (last == null)
head = p;
//last不為空,把p放到last節點後面
else {
p.before = last;
last.after = p;
}
//p為尾節點
tail = p;
++modCount;
}
}
//回撥函式,新節點插入之後回撥 , 根據evict和accessOrder判斷是否需要刪除最老/早插入的節點。
//如果實現LruCache會用到這個方法。
//removeEldestEntry制定刪除規則,JDK8中預設返回false
void afterNodeInsertion(boolean evict) { // possibly remove eldest
Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
注:HashMap定義了三個回撥方法,用於LinkedHashMap維持有序:
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
取出元素
LinkedHashMap的get方法,呼叫HashMap的getNode方法後,對accessOrder的值進行了判斷,我們之前提到:accessOrder為true時進行,把當前訪問的元素移動到連結串列尾部,呼叫重寫的afterNodeAccess方法來調整順序。
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;
}
刪除元素
LinkedHashMap刪除元素呼叫的是弗雷德remove方法,在removeNode設定了回撥方法afterNodeRemoval
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
...
afterNodeRemoval(node);
...
LinkedHashMap回撥方法的實現:
//在刪除節點e時,同步將e從雙向連結串列上刪除
void afterNodeRemoval(Node<K,V> e) { // unlink
Entry<K,V> p =
(Entry<K,V>)e, b = p.before, a = p.after;
//待刪除節點 p 的前置後置節點都置空,相當於從雙連結串列上取下來
p.before = p.after = null;
//p是尾節點,它的前一個節點為空,那麼它的後一個節點做頭節點
if (b == null)
head = a;
//p的前一個節點不為空,把它的後一個節點指向a
else
b.after = a;
//同理,a為空,b就是尾節點
if (a == null)
tail = b;
//a不為空,
else
a.before = b;
}
迭代器
abstract class LinkedHashIterator {
//記錄下一個迭代的節點
Entry<K,V> next;
//當前迭代的節點
Entry<K,V> current;
//用於fail-fast機制
int expectedModCount;
//迭代器初始化next指向head
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
//連結串列方式迭代
final Entry<K,V> nextNode() {
Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
//雙連結串列的後繼節點節點指向next
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;
}
}
利用LinkedHashMap簡單實現LRU演算法
在查閱相關資料時,都提到利用LinkedHashMap實現LRU演算法,首先介紹一下LRU(Least Recently Used)演算法:
LRU(Least recently used,最近最少使用)演算法根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是“如果資料最近被訪問過,那麼將來被訪問的機率也更高”。也就是說,當有限的空間已存滿資料時,應當把最久沒有被訪問到的資料淘汰。
根據上面的原始碼,我們知道當把accessOrder設為true時,LinkedHashMap就會根據訪問順序排序,把最近訪問過的元素放在連結串列的尾部,而沒有訪問的元素就放在連結串列頭部,只要重寫removeEldestEntry設定丟棄頭部節點條件就行了。這樣就可以簡單實現LRU演算法了
public class LRUCache<K, V> {
private final int CACHE_SIZE;
private final float DEFAULT_LOAD_FACTORY = 0.75f;
LinkedHashMap<K, V> map;
public LRUCache(int cacheSize) {
CACHE_SIZE = cacheSize;
int capacity = (int)Math.ceil(CACHE_SIZE / DEFAULT_LOAD_FACTORY) + 1;
map = new LinkedHashMap<K,V>(capacity, DEFAULT_LOAD_FACTORY, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return map.size() > cacheSize;
}
};
}
public void put(K key, V value) {
map.put(key, value);
}
public V get(K key) {
return map.get(key);
}
public void remove(K key) {
map.remove(key);
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<K, V> entry : map.entrySet()) {
stringBuilder.append(String.format("%s: %s ", entry.getKey(), entry.getValue()));
}
return stringBuilder.toString();
}
public static void main(String[] args) {
LRUCache<Integer, Integer> cache = new LRUCache<>(5);
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
System.out.println(cache);
cache.get(1);
cache.put(4,4);
cache.put(5,5);
cache.put(6,6);
System.out.println(cache);
}
}
結果:
超出範圍後,1:1鍵值對被訪問過,最早沒有被訪問過的2:2鍵值對就被丟棄了。
總結
LinkedHashMap基於HashMap,所謂大樹底下好乘涼,重寫了部分程式碼就能夠實現有序插入。LinkedHashMap總體上較為簡單,在理解了HashMap的基礎上就能很快理解LinkedHashMap的實現。
程式設計改變世界
相關文章
- JDK1.8 原始碼分析(九)--LinkedHashMapJDK原始碼HashMap
- LinkedHashMap 原始碼解讀(JDK 1.8)HashMap原始碼JDK
- java基礎:LinkedHashMap — 原始碼分析JavaHashMap原始碼
- 【JDK】JDK原始碼分析-ReentrantLockJDK原始碼ReentrantLock
- 死磕 java集合之LinkedHashMap原始碼分析JavaHashMap原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(3)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(2)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(1)JDK原始碼
- JDK原始碼分析-TreeSetJDK原始碼
- Java——LinkedHashMap原始碼解析JavaHashMap原始碼
- LinkedHashMap原始碼解讀HashMap原始碼
- 搞懂 Java LinkedHashMap 原始碼JavaHashMap原始碼
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- JDK 1.6 HashMap 原始碼分析JDKHashMap原始碼
- jdk原始碼分析之TreeMapJDK原始碼
- 死磕 jdk原始碼之HashMap原始碼分析JDK原始碼HashMap
- 原始碼分析–ArrayList(JDK1.8)原始碼JDK
- 原始碼分析–HashSet(JDK1.8)原始碼JDK
- ArrayList原始碼分析 jdk1.8原始碼JDK
- JDK 原始碼分析(1) Object類JDK原始碼Object
- HashMap原始碼分析(JDK8)HashMap原始碼JDK
- LinkedList原始碼分析(jdk1.8)原始碼JDK
- JDK1.8 hashMap原始碼分析JDKHashMap原始碼
- HashMap原始碼分析 JDK1.8HashMap原始碼JDK
- ConcurrentHashMap原始碼分析-JDK18HashMap原始碼JDK
- ArrayList原始碼分析(JDK1.8)原始碼JDK
- 原始碼分析系列1:HashMap原始碼分析(基於JDK1.8)原始碼HashMapJDK
- preact原始碼分析(四)React原始碼
- JDK1.8 原始碼分析(十) -- TreeMapJDK原始碼
- JDK1.8原始碼分析之HashMapJDK原始碼HashMap
- 原始碼|jdk原始碼之棧、佇列及ArrayDeque分析原始碼JDK佇列
- LinkedHashMap 詳解及原始碼簡析HashMap原始碼
- OkHttpClient原始碼分析(四)—— CacheInterceptorHTTPclient原始碼
- Netty原始碼分析(四):EventLoopGroupNetty原始碼OOP
- JDK1.8原始碼分析筆記-HashMapJDK原始碼筆記HashMap
- 原始碼分析–ConcurrentHashMap與HashTable(JDK1.8)原始碼HashMapJDK