LinkedHashMap在Map的基礎上進行了擴展,提供了按序訪問的能力。這個順序經過accessOrder控制,能夠是結點的插入順序,也能夠是結點的訪問時間順序。面試
LinkedHashMap還提供了removeEldestEntry方法,能夠用來刪除最老訪問結點。算法
經過accessOrder和removeEldestEntry能夠用來實現LRU緩存。緩存
如圖所示,LinkedHashMap實現順序訪問的方法比較簡單,在HashMap實現以外,還維護了一個雙向鏈表。每當插入結點時,不只要在Map中維護,還須要在鏈表中進行維護。HashMap中的put, get等方法都提供了一些鉤子方法,如afterNodeAccess
、afterNodeInsertion
和afterNodeRemoval
等。經過這些方法,LinkedHashMap能夠對這些結點進行一些特性化的維護。數據結構
當遍歷LinkedHashMap時經過遍歷鏈表代替遍歷Map中的各個槽,從而實現按序訪問。工具
/** * LinkedHashMap普通的鏈表結點,繼承了HashMap的Node,在此基礎上 * 對每一個Node添加了before和after指針。LinkedHashMap在HashMap的 * 基礎上,還維護了一個雙向鏈表,鏈表中的結點就是Map中的每一個結點, * 經過此鏈表,LinkedHashMap就實現了維護結點順序的目的 */
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);
}
}
/** * 雙向鏈表的頭結點 */
transient LinkedHashMap.Entry<K, V> head;
/** * 雙向鏈表的尾結點 */
transient LinkedHashMap.Entry<K, V> tail;
/** * true-按訪問順序(最先操做過的結點靠前) * false-按插入順序遍歷(最先插入的結點靠前) * * @serial */
final boolean accessOrder;
複製代碼
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);
// 建立一個key-value對時,不只要放入map中,還有放入LinkedHashMap
// 內置的雙向鏈表中,用來維護插入順序
linkNodeLast(p);
return p;
}
Node<K, V> replacementNode(Node<K, V> p, Node<K, V> next) {
LinkedHashMap.Entry<K, V> q = (LinkedHashMap.Entry<K, V>) p;
LinkedHashMap.Entry<K, V> t =
new LinkedHashMap.Entry<K, V>(q.hash, q.key, q.value, next);
// 用t結點代替q結點在雙向鏈表中的位置
transferLinks(q, t);
return t;
}
TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
TreeNode<K, V> p = new TreeNode<K, V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
LinkedHashMap.Entry<K, V> q = (LinkedHashMap.Entry<K, V>) p;
TreeNode<K, V> t = new TreeNode<K, V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
複製代碼
// 在雙向鏈表尾部添加結點
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;
}
}
// 使用dst結點覆蓋src結點在雙向鏈表中的位置
private void transferLinks(LinkedHashMap.Entry<K, V> src, LinkedHashMap.Entry<K, V> dst) {
LinkedHashMap.Entry<K, V> b = dst.before = src.before;
LinkedHashMap.Entry<K, V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}
/** * 每次插入新Node時,是否須要刪除最老的結點。 * * @return true-刪除最老結點,false-不刪除 */
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return false;
}
複製代碼
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
/** * 能夠指定遍歷結點的順序 * * @param accessOrder true-按訪問順序(最先操做過的結點靠前) * false-按插入順序遍歷(最先插入的結點靠前) */
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
複製代碼
// 重寫HashMap中提供給LinkedHashMap的鉤子方法
/** * HashMap 調用remove方法後,會調用這個鉤子方法,e爲刪除的結點 */
void afterNodeRemoval(Node<K, V> e) {
// p = e; b = p.before; a = p.after;
LinkedHashMap.Entry<K, V> p =
(LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
// 從雙向鏈表中刪除p結點
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
/** * HashMap 調用put等方法後,會調用這個鉤子方法 * * @param evict false-table處於建立模式(即經過構造方法調用) */
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K, V> first;
// 若是map中存在元素,且須要刪除eldest元素,則從鏈表和Map中
// 刪除雙向鏈表頭結點。removeEldestEntry在LinkedHashMap默認返回
// false。該方法能夠用來實現LRU緩存
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
/** * HashMap 調用put, get等方法後,會調用這個鉤子方法,更改最新訪問時間。 * 能夠用來實現LRU緩存 * * @param e 最近操做過的結點 */
void afterNodeAccess(Node<K, V> e) {
LinkedHashMap.Entry<K, V> last;
// 若是accessOrder爲true,表明按最新遍歷時間維護鏈表
// 則將e移至鏈表尾部
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;
}
}
複製代碼
public boolean containsValue(Object value) {
// 由於LinkedHashMap中使用雙向鏈表維護了全部Node,因此只須要遍歷
// 雙向鏈表便可遍歷全部Node。而不用遍歷Map。
for (LinkedHashMap.Entry<K, V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
public V get(Object key) {
Node<K, V> e;
// 尋找key對應結點
if ((e = getNode(hash(key), key)) == null)
return null;
// 若是須要按訪問時間排序,則更新結點在雙向鏈表中的位置
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
public V getOrDefault(Object key, V defaultValue) {
Node<K, V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
public void clear() {
super.clear();
head = tail = null;
}
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
// 覆寫了遍歷方法,用遍歷雙向鏈表代替遍歷map,從而實現了按序遍歷。
for (LinkedHashMap.Entry<K, V> e = head; e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
複製代碼
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;
}
}
複製代碼
LinkedHashMap如何實現有序的this
LinkedHashMap在HashMap的基礎上,還將每一個key-value對應的Node維護在了一個額外的雙向鏈表中。spa
LinkedHashMap經過accessOrder能夠支持按插入的順序訪問,或者按遍歷的順序訪問指針
accessOrdercode
- false: 按插入順序排序,map中每插入一個結點時,將這個結點同時放置在雙向鏈表的結尾
- true: 按訪問順序排序,當操做map中的一個結點時,經過HashMap提供的鉤子方法(
afterNodeAccess
、afterNodeInsertion
和afterNodeRemoval
)找到這個結點在鏈表中的位置,並移動到鏈表結尾。這樣鏈表的頭結點就是鏈表最久沒有訪問過的結點遍歷的時候,經過便利雙向鏈表代替遍歷map的每一個槽,來實現順序訪問。cdn
如何用LinkedHashMap實現LRU
首先分析LRU算法有哪些特性
- 新數據插入到鏈表尾部(表明最新訪問);
- 每當緩存命中(即緩存數據被訪問)則將數據移到鏈表尾部(表明最新訪問);
- 當鏈表滿的時候,將鏈表頭部的數據丟棄(刪除最久未訪問結點);
在LinkedHashMap保證結點有序的狀況下,經過設置accessOrder爲true,採用按遍歷順序維護結點。
- put方法將結點插入到雙向鏈表尾部實現LRU特性 1;
- 鉤子方法
afterNodeAccess
實現LRU特性 2;- 實現removeEldestEntry方法,刪除最久未訪問結點。實現LRU特性 3;