前言
本篇將介紹LruCache
,而LruCache
是用LinkedHashMap
實現的,LinkedHashMap
繼承HashMap
所以沒看過HashMap
的先看下我另外篇博文HashMap源碼解析(JDK8)再來看本篇。
接下來是正菜LruCache
不過吃之前我們先看下前菜LinkedHashMap
,只要LinkedHashMap
弄明白了LruCache
也就小菜一碟了,本文的LinkedHashMap
是基於JDK1.8的。
概述
LinkedHashMap
繼承至HashMap
大部分方法都直接沿用,不同點如下。
- 相對於
HashMap
的節點增加了首尾指針形成了一個雙向鏈表,並在put()
插入的時候按先後順序新添加的節點插入到鏈表的尾部,remove()
刪除的時候移除該節點保證鏈表的順序。 - 新增
accessOrder
參數,除了按插入順序維護鏈表外,還可以按訪問順序,將操作過的節點移動到尾部方便實現我們的近期最少使用算法。 - 內部是有序的,迭代的時候按鏈表順序從前往後迭代。
LruCache
內部使用LinkedHashMap
實現,在構造方法的時候會讓我們傳入能存儲的最大容量maxSize
,在新增元素的時候會調用trimToSize()
方法,方法內會將sizeOf()
方法計算得到的每個元素的大小的總和size
與構造方法傳入的maxSize
比較,如果大於maxSize
則刪除最舊的元素,即LinkedHashMap
的頭部元素,直到size
<=maxSize
。
正文
先來LinkedHashMap
源碼,再來LruCache
的。還是按照構造方法、增、刪、改、查、迭代器的順序介紹。
LinkedHashMap
首先我們要知道的是LinkedHashMap
是HashMap
的子類,大部分方法都是直接沿用HashMap
的。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
構造方法
final boolean accessOrder;//是否按照訪問順序維護鏈表順序 默認是false按照插入順序維護
transient LinkedHashMapEntry<K,V> head;//雙向鏈表的頭結點
transient LinkedHashMapEntry<K,V> tail;//雙向鏈表的尾結點
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);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {//繼承HashMap.Node並給每個結點新增before, after指針
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
總結
- 構造方法和
HashMap
差不多,就多了一個我們可以自己設置accessOrder
值的。 - 新增
head
和tail
字段,方便獲取鏈表的頭尾。 - 繼承
HashMap.Node
並給每個結點增加首尾指針,方便順序的維護。
增、改
LinkedHashMap
並沒有自己實現put()
方法還是沿用父類HashMap
的,最終會調到putVal()
方法,這個方法在HashMap源碼解析(JDK8)已經講過了,這裏我們直接看LinkedHashMap
中的不同處。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//在插入新元素的時候會調用newNode()方法創建新元素
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//在插入新元素的時候會調用newNode()方法創建新元素
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//如果存在相同key的鍵值對並且替換了舊的值則調用afterNodeAccess()方法
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);//如果插入了新的元素則調用afterNodeInsertion()方法
return null;
}
接下來看插入新元素會調用的兩個方法newNode()
和afterNodeInsertion()
。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {//創建新的節點
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);//將新的節點添加到鏈表尾部
return p;
}
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {//添加新的節點到鏈表尾部
LinkedHashMapEntry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
void afterNodeInsertion(boolean evict) { //插入新節點通知
LinkedHashMapEntry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {//判斷是否要刪除舊的節點
K key = first.key;
removeNode(hash(key), key, null, false, true);//移除節點
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {//默認返回false不會刪除舊的元素
return false;
}
從源碼可以看出newNode()
是創建新節點插入到鏈表的尾部,afterNodeInsertion()
是插入新節點的通知方法,裏面會通過evict
標記和removeEldestEntry()
方法判斷是否要移除舊的節點,默認實現是不移除。
再來看覆蓋value的通知afterNodeAccess()
方法
void afterNodeAccess(Node<K,V> e) { //移動節點到尾部
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {//如果accessOrder爲true並且覆蓋的元素不是最後一個
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;//將元素e的尾指針置爲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 {//將e添加到鏈表尾部
p.before = last;
last.after = p;
}
tail = p;//尾指針指向e
++modCount;
}
}
如果我們accessOrder
爲true並且覆蓋的節點不是最後一個節點,則將他移動到鏈表的尾部。
總結下增和改的操作
- 如果不存在key相同的節點則通過
newNode()
創建新節點插入到鏈表的尾部,然後調用afterNodeInsertion()
方法通知有新的節點插入,裏面會通過evict
標記和removeEldestEntry()
方法判斷是否要移除舊的節點,默認實現是不移除。 - 如果存在相同key的節點,則默認覆蓋該節點的值,然後調用
afterNodeAccess()
方法通知有節點被覆蓋,如果accessOrder
爲true並且覆蓋的節點不是最後一個節點,則將他移動到鏈表的尾部。
刪
LinkedHashMap
刪除還是使用的HashMap
的方法,只不過在刪除的時候調用了afterNodeRemoval()
方法通知,那我們直接看該方法的實現。
void afterNodeRemoval(Node<K,V> e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
可以看到就是將要刪除的節點前後指針置爲null,然後把前後元素的指針調整下。
查
這個方法是LinkedHashMap
自己的,不過內部還是調用的HashMap
的getNode()
方法獲取節點
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)//調用父類的getNode()獲取節點
return null;
if (accessOrder)//如果accessOrder爲true
afterNodeAccess(e);//將訪問的元素移動到尾部
return e.value;
}
可以發現如果accessOrder
爲true則調用afterNodeAccess()
方法,這個方法在前面增的時候說過會將傳入的節點放到鏈表的尾部。
迭代
public Set<Map.Entry<K,V>> entrySet() {//獲取entrySet
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {//entrySet對象
...
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(); }
}
abstract class LinkedHashIterator {
LinkedHashMapEntry<K,V> next;
LinkedHashMapEntry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
next = head;//從頭部開始迭代
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
final LinkedHashMapEntry<K,V> nextNode() {//迭代方法 按鏈表順序從前往後迭代
LinkedHashMapEntry<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
是有序的,迭代是按鏈表插入順序從前往後迭代。
總結
這裏對LinkedHashMap
做一個總結
LinkedHashMap
內部維護了一個雙向鏈表,保證了LinkedHashMap
是一個有序的哈希表- 在構造方法的時候可以將
accessOrder
置爲true,使LinkedHashMap
按訪問順序維護鏈表的順序 - 迭代效率較高,直接從鏈表的頭部到尾部依次迭代。
LruCache
看了LinkedHashMap
再來看LruCache
就非常簡單了。
構造方法
private int size;//當前lrucache大小
private int maxSize;//最大容量
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);//使用LinkedHashMap存儲元素,並且將accessOrder置爲了true
}
在構造方法的時候我們傳入該LruCache
最大的容量maxSize
,然後初始化LinkedHashMap
作爲容器存儲元素並且將accessOrder置爲了true。再來看一個計算每個元素size的關鍵方法sizeOf()
protected int sizeOf(K key, V value) {
return 1;
}
這個方法需要我們自己實現,用來計算每個添加進LruCache
的元素的size。並且這個size的單位和構造方法傳入的maxSize
單位要保持一致,因爲後面會用這兩個值進行比對判斷是否要移除舊的元素。
增、改
public final V put(K key, V value) {//添加新的元素
if (key == null || value == null) {//邊界判斷
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);//計算添加元素大小,添加進size變量
previous = map.put(key, value);//添加到LinkedHashMap
if (previous != null) {//如果之前有添加過
size -= safeSizeOf(key, previous);//減去之前元素的size
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);//通知有元素移除
}
trimToSize(maxSize);//判斷是否超出maxSize要移除舊的元素
return previous;
}
private int safeSizeOf(K key, V value) {//計算每個元素的size
int result = sizeOf(key, value);//實際調用sizeOf獲取元素大小
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
public void trimToSize(int maxSize) {//如果超出容量則移除舊的元素
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {//判斷size是否超過maxSize
break;
}
Map.Entry<K, V> toEvict = map.eldest();//拿到LinkedHashMap最舊的元素即頭部
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);//移除
size -= safeSizeOf(key, value);//減去移除元素的size
evictionCount++;
}
entryRemoved(true, key, value, null);//通知有元素移除
}
}
public Map.Entry<K, V> eldest() {//LinkedHashMap中最舊的元素head
return head;
}
增加和修改的元素先通過sizeOf()
方法計算要加入元素大小添加到size
字段,然後把元素添加進map,如果之前有key相同的元素則size
減去之前元素的大小,然後調用trimToSize()
方法判斷size
是否大於了maxSize
如果大於了,則刪除map中最舊的元素直到size
小於等於maxSize
。
刪
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);//移除節點
if (previous != null) {
size -= safeSizeOf(key, previous);//減去移除節點的size
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);//通知有元素移除
}
return previous;
}
查
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);//獲取到的mapValue爲null則調用create()方法,create()默認返回的null
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);//將create()方法生成的值插入map
if (mapValue != null) {//插入位置原先有值
map.put(key, mapValue);//那麼不插入我們create()方法生成的值
} else {//原先位置沒值
size += safeSizeOf(key, createdValue);//計算插入值的大小賦值給size
}
}
if (mapValue != null) {//原先位置有值
entryRemoved(false, key, createdValue, mapValue);//通知createdValue插入失敗
return mapValue;
} else {//插入了新的值
trimToSize(maxSize);//判斷是否要刪除舊的元素
return createdValue;
}
}
protected V create(K key) {
return null;
}
一般情況下就是直接通過LinkedHashMap
的get()
方法直接查詢元素。特殊情況如果沒找到key對應的元素並且實現了create()
方法,判斷key對應位置是否有元素,如果沒有則插入create()
方法創建的元素,將元素大小添加到size
,然後調用trimToSize(maxSize)
調整判斷是否要刪除舊的元素。
總結
這裏對LruCache
做一個總結
- 內部通過
LinkedHashMap
實現,maxSize
和sizeOf()
方法返回值單位要一致。 - 每次添加元素的時候通過
sizeOf()
方法計算每個元素的大小,當size
大於maxSize
的時候會刪除LinkedHashMap
中最舊的元素即頭部,直到size
<=maxSize
。