PS: 要先了解HashMap的實現原理HashMap源碼分析
一、簡單介紹
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
可以看到LinkedHashMap繼承了HashMap,其實際是在HashMap基礎上,把數據節點連成一個雙向鏈表,遍歷的時候按鏈表順序遍歷。
小總結
默認的LinkedHashMap 的遍歷會按照插入的順序遍歷出來,HashMap則不保證順序。
注意上面是默認的情況,LinkedHashMap中還有個accessorder成員標誌,默認是false,當爲true時,每get一個元素,都會把這個元素放在鏈表最後,即遍歷的時候就變成最後被遍歷出來。
二、源碼分析
//通過繼承HashMap.Node類,新增兩個成員,before, after實現鏈表
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類,新增了兩個成員before,after,用於實現鏈表結構。
類成員
//指向鏈表的頭結點
transient LinkedHashMap.Entry<K,V> head;
//指向鏈表的尾節點
transient LinkedHashMap.Entry<K,V> tail;
//爲false時,遍歷會按插入的順序遍歷
//爲true時,每一次get操作,都會把獲得的節點放到鏈表尾
//默認爲false
final boolean accessOrder;
接下來就是分析從增刪查來讀LinkedHashMap源碼。
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
這三個方法是父類HashMap留給子類實現的方法,分別在get、put、remove方法完成後調用
1. 新增操作
一開始筆者想找put方法,但發現其並沒有重寫父類的put方法,轉去找Entry類在哪裏使用到。找到以下兩個方法。
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;
}
//紅黑樹結構時用到
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的put方法中,如果是一個當前不存在的key,就會根據不同結構分別調用上面兩個方法來創建新節點,這樣就能把父類HashMap裏的節點換成LinkHashMap中的通過“改裝”的節點了。
接下來看linkNodeLast()方法操作實現鏈表
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;//舊的尾節點下一個節點指向新的尾節點
}
}
就是把新增的節點放在鏈表最後,如果鏈表一開始爲空,那就賦值給頭結點
再看看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);
}
}
由於removeEldestEntry()方法總是返回false,所以該方法等於沒有做任何操作。
小總結
LinkedHashMap的新增操作通過父類HashMap來完成,它則是通過定以義一個Entry繼承父類Node的,然後利用Entry實現了鏈表的結構。
2. 查操作
2.1 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;
}
getNode()方法在父類HashMap中實現了,LinkedHashMap重用其來查詢數據,然後根據accessOrder的值來決定是否需要調整鏈表.
下面看afterNodeAccess()方法的做法
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
//進入條件:accessOrder爲true,且尾節點不等於e
LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
//p就是節點e,b是節點的前節點,a是e的後節點
p.after = null;//先把p的節點賦值爲null
if (b == null)//說明p是頭結點
head = a;
else
b.after = a;//把p的前節點連上p的後節點
if (a != null)//這裏肯定不爲null吧??
a.before = b;//把p的後節點連上p的前節點
else
last = b;
if (last == null)
head = p;
else {
p.before = last;//把p的前節點指向尾節點
last.after = p;//把舊尾節點的後節點指向p
}
tail = p;//尾節點賦值爲p
++modCount;
}
}
afterNodeAccess()方法做的就是,把傳入節點e,移動到鏈表尾部。
2.2 遍歷操作
來看看其遍歷如何實現,挑entrySet方法來說,先回顧下遍歷寫法
Iterator it = map.entrySet().iterator();
while(it.hasNext())
it.next();
追蹤其源碼如下
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() {
return nextNode();
}
}
可以看到map.entrySet().iterator()這一句代碼返回的是LinkedEntryIterator對象。而其next()方法,則是調用了nextNode()方法,該方法在父類LinkedHashIterator中實現了
LinkedHashIterator() {
next = head;//next一開始指向鏈表頭結點
expectedModCount = modCount;
current = null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
//next一開始是指向head(即指向鏈表頭)
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)//快速失敗
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;//next指向下一個節點
return e;
}
可以看到遍歷如前文所說,是遍歷鏈表。
3. 刪除操作
LinkedHashMap沒有重寫remove方法,而是重寫了afterNodeRemoval方法
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
//把p的前後指向賦值爲null
p.before = p.after = null;
if (b == null)//說明是頭結點
head = a;
else
b.after = a;//p原來的前節點指向p原來的後節點
if (a == null)//說明p是尾節點
tail = b;
else
a.before = b;//p原來的後節點指向p原來的前節點
}
LinkedHashMap重用了HashMap的remove方法,然後在afterNodeRemoval方法中刪除鏈表中相應的節點
三、實現LRU算法(leetcode的一道題)
利用LinkedHashMap的數據結構特性,可以簡便地實現LRU算法。
class LRUCache {
private LinkedHashMap<Integer,Integer> data;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
//這裏要指定第三個參數爲true
data=new LinkedHashMap<>(capacity,1,true);
}
public int get(int key) {
//get()方法會自動調整鏈表
Integer o = data.get(key);
return o!=null?o:-1;
}
public void put(int key, int value) {
//這裏要調用一次get方法,一來可以檢查當前key是否存在
//二來可以調整其在鏈表位置的位置
if(data.get(key)==null&&data.size()==capacity){
data.remove(data.keySet().iterator().next());
}
data.put(key,value);
}
}
這裏只展示了實現插入整數,可改進爲泛型使其更加通用。