一. 回顧
前面深入瞭解了HashMap實現原理以及HashMap常見問題,今天簡略瞭解LinkedHashMap原理。
此筆記僅供自己學習完後複習回顧參考,還有很多待提高的地方,如有錯誤請指正
二. LinkedHashMap
打開IDEA,按兩下shift搜索LinkedHashMap,打開源碼,可以看到如下:
* @since 1.4
*/
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
解釋: @since1.4代表LinkedHashMap在jdk1.4纔開始有。LinkedHashMap繼承了HashMap,實現了Map接口。由此可以看出LinkedHashMap是一個HashMap,推理出其在HashMap的基礎上增加了某些東西。
2.1 成員變量
head、tail: 從中文意思可看出這是節點的頭尾指針,也就是在HashMap的基礎上加了頭尾指針。 指針的類型是LinkedHashMap.Entry<K,V>
,它是LinkedHashMap的靜態內部類,後面再詳述。
/**
* 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;
accessOrder: 從中文意思可看出這是訪問順序。這也是在HashMap的基礎上加的東西。註釋中作出瞭解釋,這是給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;
2.2 構造方法
因爲LinkedHashMap繼承了HashMap,所以兩者底層架構並沒有很大的差異(同樣都是數組+鏈表+紅黑樹)。
兩者的差別就在:LinkedHashMap的數據節點多了2個指針:頭指針head
和尾指針tail
它有5個構造方法,比HashMap多了1個,其他的四個構造方法和HashMap的都差不多。如下:
LinkedHashMap(): 無參構造,調用父類HashMap的無參構造(初始容量爲16,負載因子爲0.75),並且設置accessOrder=false;
(即使用插入順序)
/**
* 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;
}
LinkedHashMap(int initialCapacity): 調用父類HashMap(initialCapacity),指定初始容量,並且設置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;
}
LinkedHashMap(int initialCapacity, float loadFactor): 調用父類HashMap(initialCapacity, loadFactor),指定初始容量和負載因子,並且設置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;
}
LinkedHashMap(Map<? extends K, ? extends V> m): 調用父類HashMap的空參構造,並且設置accessOrder=false;
(即使用插入順序),調用putMapEntries()方法將map集合轉爲LinkedHashMap集合。容量由入參map集合決定
/**
* Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
* the same mappings as the specified map. The <tt>LinkedHashMap</tt>
* instance is created with a default load factor (0.75) and an initial
* capacity sufficient to hold the mappings in the specified map.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder): 調用父類HashMap的(initialCapacity, loadFactor)
,並且設置指定的accessOrder
(即使用指定的順序)。此三個入參的構造方法在HashMap並沒有
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
2.3 靜態內部類
這是增加了head、tail兩個指針的類型,繼承了HashMap.Node<K,V>,也就是說擁有HashMap.Node<K,V>的特性。構造方法調用了HashMap.Node<K,V>的Node(int hash, K key, V value, Node<K,V> next)
/**
* 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);
}
}
2.4 linkNodeLast()方法
在鏈表末尾添加節點,如下:
// 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加了head、tail的指針,作用和C語言的鏈表差不多。
2.5 訪問順序
上面介紹成員變量accessOrder
的時候,涉及到訪問順序
這個詞。
在解釋這個知識點之前,先回顧一下LRU知識點
。
LRU:Least Recently Used,中文是最近最少使用
。是一種常用的頁面置換算法
。在內存不命中的條件下,內存缺頁需要將頁面置換。詳情如下:
下面用一個測試實例來解釋說明。
下面的測試目的是證明:
- HashMap插入的順序與取出的順序是不一樣的,即HashMap是無序的
- LinkedHashMap插入的順序與取出的順序是一樣的,即LinkedHashMap是有序的
@Test
public void test1(){
Map<String, String> linkMap = new LinkedHashMap<String, String>();
Map<String, String> hashMap = new HashMap<String, String>();
linkMap.put("2", "2");
linkMap.put("1", "1");
linkMap.put("3", "3");
hashMap.put("2", "2");
hashMap.put("1", "1");
hashMap.put("3", "3");
Set<Map.Entry<String, String>> linkEntries = linkMap.entrySet();
Set<Map.Entry<String, String>> hashEntries = hashMap.entrySet();
System.out.print("LinkedHashMap:");
for (Map.Entry<String, String> entry : linkEntries) {
System.out.print(" " + entry + " ");
}
System.out.println();
System.out.print("HashMap:");
for (Map.Entry<String, String> entry : hashEntries) {
System.out.print(" " + entry + " ");
}
}
測試結果:
下面測試開啓accessOrder
訪問順序(即accessOrder=true
)。如下:
@Test
public void test2() {
//使用三個入參的構造器來設置accessOrder=true
Map<String, String> linkMap =
new LinkedHashMap<String, String>(16, 0.75f, true);
linkMap.put("2", "2");
linkMap.put("1", "1");
linkMap.put("3", "3");
Set<Map.Entry<String, String>> linkEntries = linkMap.entrySet();//獲取鍵值對集合
System.out.print("訪問前的LinkedHashMap:");
for (Map.Entry<String, String> entry : linkEntries) {
System.out.print(" " + entry + " ");
}
System.out.println();
//訪問linkMap
linkMap.get("2");
System.out.print("訪問2後的LinkedHashMap:");
for (Map.Entry<String, String> entry : linkEntries) {
System.out.print(" " + entry + " ");
}
System.out.println();
//訪問linkMap
linkMap.get("1");
System.out.print("訪問1後的LinkedHashMap:");
for (Map.Entry<String, String> entry : linkEntries) {
System.out.print(" " + entry + " ");
}
}
測試結果:
可以看到被訪問後的元素,被置頂了。也就是說,LinedHashMap訪問某元素,某元素將被置頂。這樣的功能應用在LRU
緩存方面。 經常訪問的元素就可以很快速地找到了,而不被經常訪問的元素可以沉到底甚至可以被覆蓋,LinkedHahsMap這樣的功能使得它在緩存技術方面發揚光大。
總結:從上面的訪問順序,也可以總結出,遍歷LinkedHashMap是按照從鏈表開始掃描,掃到尾部的。get元素後,元素被放到鏈表的尾部,所以每次get後再遍歷LinkedHashMap,get的元素都會出現在末尾。
2.6 LinkedHashMap是如何實現LRU算法?
Mybatis中的Lrucache底層是用LinkedHashMap。我們從源碼分析LinkedHashMap怎麼可以實現LRU。
因爲涉及的是訪問順序,而它又是因get()方法引起的,所以首先從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;
}
發現與afterNodeAccess(e)
有關,點進去看看,如下:
void afterNodeAccess(Node<K,V> e) { // move node to last
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;
}
}
看到方法開頭就已經有一行註釋了// move node to last
,意思就是將元素移到鏈表尾部。
總結:當訪問某元素,如果設置了訪問順序accessOrder=true
,則該元素被移到鏈表尾部。
按照這個思路,如果滿足當前長度大於某值就移除鏈頭的元素,這樣就實現了LRU緩存。
由此可推理當我們put元素進去,LinkedHashMap會判斷元素是否已經達到了LRU設定的緩存大小,如果達到就會實行LRU。將很久不被使用的元素移除。因此我們查看LinkedHashMap的put()
方法,發現沒有這個方法。那說明LinkedHashMap的put方法調用的是父類HashMap的put()
方法。打開HashMap找到put()
方法,發現有一個afterNodeInsertion()
,如下:
上圖中此afterNodeInsertion()
方法並沒有在HashMap中實現而是在LinkedHashMap實現。
打開LinkedHashMap查看afterNodeInsertion()
方法,如下:
點進去看removeEldestEntry(first)
該方法默認是返回false(即默認不實現緩存功能)。如下:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
總結:綜上所述只要我們重寫這個方法在某條件下返回true就可以實現LRU緩存。
測試例子:用LinkedHashMap實現LRU緩存
@Test
public void test3() {
Map<String, String> map =
new LinkedHashMap<String, String>(5, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
//當前長度>=5,再插入就移除。即達到閾值時就實現LRU
return this.size()>=5;
}
};
map.put("2", "2");
map.put("1", "1");
map.put("3", "3");
map.put("9", "9");
Set<Map.Entry<String, String>> linkEntries = map.entrySet();//獲取鍵值對集合
//訪問linkMap
map.get("2");
System.out.print("訪問2後的LinkedHashMap:");
for (Map.Entry<String, String> entry : linkEntries) {
System.out.print(" " + entry + " ");
}
System.out.println();
map.put("5", "5");
System.out.print("存入5後的LinkedHashMap:");
for (Map.Entry<String, String> entry : linkEntries) {
System.out.print(" " + entry + " ");
}
System.out.println();
}
測試結果: