前言
LinkedHashMap繼承
自HashMap。和HashMap不同的是,它維護一條雙向鏈表
,解決了遍歷順序和插入順序一致的問題。並且還對訪問順序提供了相應的支持。因爲LinkedHashMap的很多實現是基於HashMap實現的,所以如果要讀懂LinkedHashMap還是需要先了解HashMap。可以參考我的這篇文章
/**
* 指向雙向鏈表的頭結點(最老的結點)
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 指向雙向鏈表的尾結點(最年輕的結點)
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* LinkedHashMap的迭代排序方法,如果是true,是進入順序,如果爲false,爲插入順序
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder;
1 常量介紹
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the default initial capacity (16) and load factor (0.75).
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and a default load factor (0.75).
*
* @param initialCapacity the initial capacity
* @throws IllegalArgumentException if the initial capacity is negative
*/
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
2 鏈表結點的插入過程(LinkedHashMap保持順序的原理)
首先是LinkedHashMap的插入過程。使用put方法,LinkedHashMap本身並沒有重寫自己繼承的put方法,它用的還是HashMap中的put方法,那究竟是怎麼做到不重寫put方法就保證了插入的順序了呢?
LinkedHashMap.Entry
/**
* HashMap.Node的子類
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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);
}
}
原本HashMap的Node結點肯定不會記錄插入順序
,但是上面的Entry結點就不一樣了,它可以記錄自己的上一個結點和下一個結點,記錄插入的順序。那這個結點是怎麼添加進去的呢?那就不得不說另一個重要的方法了。LinkedHashMap通過重寫HashMap的newNode方法
,在HashMap添加結點時調用了子類LinkedHashMap的newNode方法
,實現Entry結點的插入。這樣既添加了雙向鏈表的需求又沒有破壞原本HashMap原本的方法。
LinkedHashMap#newNode
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;
}
LinkedHashMap#linkNodeLast
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
//將尾結點複製給last
LinkedHashMap.Entry<K,V> last = tail;
//新添加的結點爲尾結點
tail = p;
//如果是第一次添加,尾結點也是頭結點
if (last == null)
head = p;
//否則
else {
//將上一個尾結點賦值給新添加結點的上一個結點,插入結點的前驅
p.before = last;
//上一個尾結點的後繼是當前尾結點
last.after = p;
}
}
在HashMap中結點的類型有兩種,一種是Node普通結點
,一種是TreeNode樹節點
,Entry繼承了HashMap.Node,可以無縫的加入,那麼TreeNode呢?我們來看看HashMap中TreeNode結點的定義。
HashMap.TreeNode
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
....
}
我們可以看到,HashMap中的TreeNode結點
居然繼承了LinkedHashMap.Entry結點
,因爲這個緣故,當然可以直接在LinkedHashMap中使用TreeNode了。
LinkedHashMap#newTreeNode
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;
}
補充
當使用HashMap的putVal方法時還會調用一個在LinkedHashMap中重要的操作afterNodeInsertion
HashMap#putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
....
++modCount;
if (++size > threshold)
resize();
//注意這個操作
afterNodeInsertion(evict);
return null;
}
LinkedHashMap#afterNodeInsertion
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);
}
}
LinkedHashMap#removeEldestEntry
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
當我們基於 LinkedHashMap 實現緩存時,通過覆寫removeEldestEntry
方法可以實現自定義策略的 LRU 緩存。
3 鏈表結點的刪除過程
LinkedHashMap結點的刪除操作同樣的是使用父類HashMap內的相關方法(remove,removeNode),而沒有重寫相關方法。那該方法又怎麼保持刪除結點後鏈表的維護呢?讓我們來看相關代碼。
HashMap#remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
HashMap#removeNode
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//p表示頭結點或者將要被刪除的結點的上一個結點
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果table不爲空,長度>0,table中hash對應下標的第一個結點不爲null,有值
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node指將要被刪除的結點
Node<K,V> node = null, e; K k; V v;
//與上面相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//鏈表頭結點不等,且後面還有結點
else if ((e = p.next) != null) {
//樹節點
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//賦值給node
node = e;
break;
}
//賦值給p,方便後面操作
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果是樹節點
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//如果頭結點是將要被刪除的結點
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
//刪除結點後維護鏈表的關鍵操作!!!!!!
afterNodeRemoval(node);
return node;
}
}
return null;
}
刪除結點後關鍵的操作是afterNodeRemoval
方法,對於HashMap來說,該方法未做任何操作
。LinkedHashMap重寫了該方法,實現了刪除結點後鏈表的維護操作。
LinkedHashMap#afterNodeRemoval
void afterNodeRemoval(Node<K,V> e) { // unlink
//複製刪除結點e,並記錄刪除結點的上一個結點和下一個結點
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//將刪除結點指向前結點和後結點的引用清空
p.before = p.after = null;
//如果刪除結點的前一個結點爲空,表明是第一個結點,改變head指向刪除結點的下一個結點
if (b == null)
head = a;
//否則前面存在結點,(刪除節點的上一個結點的after)指向(刪除結點的下一個結點)
else
b.after = a;
//如果刪除結點爲尾結點,改變tail指向刪除結點的上一個結點
if (a == null)
tail = b;
//否則改變(刪除結點的下一個結點的before)指向(刪除結點的上一個結點)
else
a.before = b;
}
4 鏈表結點的獲得過程
LinkedHashMap#get
/**
* 返回指定鍵的值
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
* 如果map中保存着相應的鍵,返回相應的值,否則返回null
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
* //返回null不一定代表不存在鍵
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*/
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//如果爲true,將結點移動到鏈表末尾
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
LinkedHashMap#afterNodeAccess
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//accessOrder=true並且訪問的結點不是尾結點
if (accessOrder && (last = tail) != e) {
//複製訪問結點e,並記錄刪除結點的上一個結點和下一個結點
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//訪問節點的after置空(變成尾結點)
p.after = null;
//如果當前結點e爲頭結點,更換頭結點爲下一個結點
if (b == null)
head = a;
//否則,當前結點的上一個結點的after指向當前結點的下一個結點
else
b.after = a;
//如果當前結點不是尾結點,當前結點的下一個結點的before指向當前節點的上一個結點
if (a != null)
a.before = b;
//否則,last指向當前結點的上一個結點(這裏是爲了適配非尾結點的狀況,p.before = last)
else
last = b;
//如果既是頭結點又是尾結點,頭結點指向當前結點(上面頭結點指向空)
if (last == null)
head = p;
//否則
else {
//當前結點的上一個結點是尾結點
p.before = last;
//尾結點的下一個結點是當前結點(至此將訪問的當前結點移動到鏈表尾部)
last.after = p;
}
//尾結點指向當前結點
tail = p;
++modCount;
}
}
測試
Map<String, String> linkedHashMap = new LinkedHashMap<>(4, 0.75f, true);
linkedHashMap.put("test1", "fjx1");
linkedHashMap.put("test2", "fjx2");
linkedHashMap.put("test3", "fjx3");
System.out.println("開始時順序:");
Set<Map.Entry<String, String>> set = linkedHashMap.entrySet();
set.forEach((entry)-> System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()));
System.out.println("通過get方法,導致key爲test1對應的Entry到表尾");
linkedHashMap.get("test1");
Set<Map.Entry<String, String>> set2 = linkedHashMap.entrySet();
set2.forEach((entry)-> System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()));