概述
LinkedHashMap是Java中常用的數據結構之一,安卓中的LruCache緩存,底層使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少使用算法,核心思想就是當緩存滿時,會優先淘汰那些近期最少使用的緩存對象
LruCache的緩存算法
LruCache采用的緩存算法為LRU(Least Recently Used),最近最少使用算法。核心思想是當緩存滿時,會首先把那些近期最少使用的緩存對象淘汰掉
LruCache的實現
LruCache底層就是用LinkedHashMap來實現的。提供 get 和 put 方法來完成對象的添加和獲取
LinkedHashMap與HashMap的區別
相同點:
1. 都是key,value進行添加和獲取
2. 底層都是使用數組來存放數據
不同點:
1. HashMap是無序的,LinkedHashMap是有序的(插入順序和訪問順序)
2. LinkedHashMap內存的節點存放在數據中,但是節點內部有兩個指針,來完成雙向鏈表的操作,來保證節點插入順序或者訪問順序
LinkedHashMap的使用
LinkedHashMap插入順序的演示代碼
public static void main(String[] args) {
//默認記錄的就是插入順序
Map<String, String> map = new LinkedHashMap<>();
map.put("name", "tom");
map.put("age", "34");
map.put("address", "beijing");
Iterator iterator = map.entrySet().iterator();
//遍歷
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("Key = " + key + ", Value = " + value);
}
}
輸出如下:
Key = name, Value = tom
Key = age, Value = 34
Key = address, Value = beijing
由輸出可以看到
我們往LinedHashMap中分別按順序插入了name,age,address以及對應的value
遍歷的時候,也是按順序分別輸出了 name,age,address以及對應的value
所以可以,LinkedHashMap默認記錄的就是插入的順序
作為比較,我們再看來一下 HashMap 的遍歷是不是有序的。就以上面這幾個值為例
代碼以及輸出如下:
HashMap的遍歷
public static void main(String[] args) {
//插入和上面一樣的值
Map<String, String> map = new HashMap<>();
map.put("name", "tom");
map.put("age", "34");
map.put("address", "beijing");
Iterator iterator = map.entrySet().iterator();
//遍歷
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("Key = " + key + ", Value = " + value);
}
}
輸出如下
Key = address, Value = beijing
Key = name, Value = tom
Key = age, Value = 34
從上面可以得知:
- HashMap遍歷的時候,是無序的,和插入的順序是不相關的。
- LinkedHashMap默認的順序是記錄插入順序
既然LinkedHashMap默認是按着插入順序的,那么肯定也有其它的順序。
對的,LinkedHashMap還可以記錄訪問的順序。訪問過的元素放在鏈表前面
遍歷的時候最近訪問的元素最后才遍歷到
LinkedHashMap的訪問順序的演示代碼
public static void main(String[] args) {
/**
* 第一個參數:數組的大小
* 第二個參數:擴容因子,和HashMap一樣,添加的元素的個數達到 16 * 0.75時,開始擴容
* 第三個參數:true:表示記錄訪問順序
* false:表示記錄插入順序(默認的順序)
*/
Map<String, String> map = new LinkedHashMap<>(16,0.75f,true);
//分別插入下面幾個值
map.put("name", "tom");
map.put("age", "34");
map.put("address", "beijing");
//既然演示訪問順序,我們就訪問其中一個元素,這里只是打印一下
System.out.println("我是被訪問的元素:" + map.get("age"));
//訪問完后,我們再遍歷,注意輸出的順序
Iterator iterator = map.entrySet().iterator();
//遍歷
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("Key = " + key + ", Value = " + value);
}
}
輸出如下:
我是被訪問的元素:34
Key = name, Value = tom
Key = address, Value = beijing
Key = age, Value = 34
從上面可以得知:
插入元素完成之后,我們訪問了age,並打印其值
之后再遍歷,因為記錄的是訪問順序,LinkedHashMap會把最近使用的元素放到最后面,所以遍歷的時候,本來age是第二次插入的,但是遍歷的時候,卻是最后一個遍歷出來的。
LruCache就是利用了LinkedHashMap的這種性質,最近使用的元素都放在最后
最近不使用的元素自然就在前面,所以緩存滿了的時候,刪除前面的。新添加元素的時候放在最后面
LinkedHashMap的原理
LinkedHashMap,望文生義 Link + HashMap,Link就鏈表
所以LinkedHashMap就是底層使用數組存放元素,使用鏈表維護插入元素的順序
使用一張圖來說明LinkedHashMap的原理
LinkedHashMap中的節點如下圖
下面我們就來手寫這樣一個結構QLinkedHashMap
首先定義節點的結構,我們就叫QEntry,如下
static class QEntry<K, V> {
public K key; //key
public V value; //value
public int hash; //key對應的hash值
public QEntry<K, V> next; //hash沖突時,構成一個單鏈表
public QEntry<K, V> before; //當前節點的前一個節點
public QEntry<K, V> after; //當前節點的后一個節點
QEntry(K key, V value, int hash, QEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
//刪除當前節點
private void remove() {
//當前節點的上一個節點的after指向當前節點的下一個節點
this.before.after = after;
//當前節點的下一個節點的before指向當前節點的上一個節點
this.after.before = before;
}
//將當前節點插入到existingEntry節點之前
private void addBefore(QEntry<K, V> existingEntry) {
//插入到existingEntry前,那么當前節點后一個節點指向existingEntry
this.after = existingEntry;
//當前節點的上一個節點也需要指向existingEntry節點的上一個節點
this.before = existingEntry.before;
//當前節點的下一個節點的before也得指向自己
this.after.before = this;
//當前節點的上一個節點的after也得指向自己
this.before.after = this;
}
//訪問了當前節點時,會調用這個函數
//在這里面就會處理訪問順序和插入順序
void recordAccess(QLinkedHashMap<K, V> m) {
QLinkedHashMap<K, V> lm = (QLinkedHashMap<K, V>) m;
//如果accessOrder為true,也就是訪問順序
if (lm.accessOrder) {
//把當前節點從鏈表中刪除
remove();
//再把當前節點插入到雙向鏈表的尾部
addBefore(lm.header);
}
}
}
我們的QLinkedHashMap中的鏈表用的是雙向循環鏈表
如圖下:
由上圖可以知道,圖中是一個雙向鏈表,頭節點和A,B兩個鏈表
其中
header.before指向最后一個節點
header.after 指向header節點
其中 QLinkedHashMap的大部分代碼和手寫Java HashMap核心源碼 一樣。
可以先看看手寫Java HashMap核心源碼一章節
QLinkedHashMap全部源碼以及注釋如下:
public class QLinkedHashMap<K, V> {
private static int DEFAULT_INITIAL_CAPACITY = 16; //默認數組的大小
private static float DEFAULT_LOAD_FACTOR = 0.75f; //默認的擴容因子
private QEntry[] table; //底層的數組
private int size; //數量
//下面這兩個屬性是給鏈表用的
//true:表示按着訪問的順序保存 false:按照插入的順序保存(默認的方式)
private boolean accessOrder;
//雙向循環鏈表的表頭,記住,這里只有一個頭指針,沒有尾指針
//所以需要用循環鏈表來實現雙向鏈表
//即:從前可以往后遍歷,也可以從后往前遍歷
private QEntry<K, V> header;
public QLinkedHashMap() {
//創建DEFAULT_INITIAL_CAPACITY大小的數組
table = new QEntry[DEFAULT_INITIAL_CAPACITY];
size = 0;
//默認按照插入的順序保存
accessOrder = false;
//初始化
init();
}
/**
*
* @param capcacity 數組的大小
* @param accessOrder 按照何種順序保存
*/
public QLinkedHashMap(int capcacity, boolean accessOrder) {
table = new QEntry[capcacity];
size = 0;
this.accessOrder = accessOrder;
init();
}
//這里主要是初始化雙向循環鏈表
private void init() {
//新建一個表頭
header = new QEntry<>(null, null, -1, null);
//鏈表為空的時候,只有一個頭節點,所以頭節點的下一個指向自己,上一個節點也指向自己
header.after = header;
header.before = header;
}
//插入一個鍵值對
public V put(K key, V value) {
if (key == null)
throw new IllegalArgumentException("key is null");
//拿到key的hash值
int hash = hash(key.hashCode());
//存在數組中的哪個位置
int i = indexFor(hash, table.length);
//看看有沒有key是一樣的,如果有,替換掉舊掉,把新值保存起來
//如調用了兩次 map.put("name","tom");
// map.put("name","jim"); ,那么最新的name對應的value就是jim
QEntry<K, V> e = table[i];
while (e != null) {
//查看有沒有相同的key,如果有就保存新值,返回舊值
if (e.hash == hash && (key == e.key || key.equals(e.key))) {
V oldValue = e.value;
e.value = value;
//重點就是這一句,找到了相同的節點,也就是訪問了一次
//如果accessOrder是true,就要把這個節點放到鏈表的尾部
e.recordAccess(this);
//返回舊值
return oldValue;
}
//繼續下一個循環
e = e.next;
}
//如果沒有找到與key相同的鍵
//新建一個節點,放到當前 i 位置的節點的前面
QEntry<K, V> next = table[i];
QEntry newEntry = new QEntry(key, value, hash, next);
//保存新的節點到 i 的位置
table[i] = newEntry;
//把新節點添加到雙向循環鏈表的頭節點的前面,
//記住,添加到header的前面就是添加到鏈表的尾部
//因為這是一個雙向循環鏈表,頭節點的before指向鏈表的最后一個節點
//鏈表的最后一個節點的after指向header節點
//剛開始我也以為是添加到了鏈表的頭部,其實不是,是添加到了鏈表的尾部
//這點可以參考圖好好想想
newEntry.addBefore(header);
//別忘了++
size++;
return null;
}
//根據key獲取value,也就是對節點進行訪問
public V get(K key) {
//同樣為了簡單,key不支持null
if (key == null) {
throw new IllegalArgumentException("key is null");
}
//對key進行求hash值
int hash = hash(key.hashCode());
//用hash值進行映射,得到應該去數組的哪個位置上取數據
int index = indexFor(hash, table.length);
//把index位置的元素保存下來進行遍歷
//因為e是一個鏈表,我們要對鏈表進行遍歷
//找到和key相等的那個QEntry,並返回value
QEntry<K, V> e = table[index];
while (e != null) {
//看看數組中是否有相同的key
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
//訪問到了節點,這句很重要,如果有相同的key,就調用recordAccess()
e.recordAccess(this);
//返回目標節點的值
return e.value;
}
//繼續下一個循環
e = e.next;
}
//沒有找到
return null;
}
//返回一個迭代器類,遍歷用
public QIterator iterator(){
return new QIterator(header);
}
//根據 h 求key落在數組的哪個位置
static int indexFor(int h, int length) {
//或者 return h & (length-1) 性能更好
//這里我們用最容易理解的方式,對length取余數,范圍就是[0,length - 1]
//正好是table數組的所有的索引的范圍
h = h > 0 ? h : -h; //防止負數
return h % length;
}
//對hashCode進行運算,JDK中HashMap的實現,直接拷貝過來了
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//定義一個迭代器類,方便遍歷用
public class QIterator {
QEntry<K,V> header; //表頭
QEntry<K,V> p;
public QIterator(QEntry header){
this.header = header;
this.p = header.after;
}
//是否還有下一個節點
public boolean hasNext() {
//當 p 不等於 header的時候,說明還有下一個節點
return p != header;
}
//如果有下一個節點,獲取之
public QEntry next() {
QEntry r = p;
p = p.after;
return r;
}
}
static class QEntry<K, V> {
public K key; //key
public V value; //value
public int hash; //key對應的hash值
public QEntry<K, V> next; //hash沖突時,構成一個單鏈表
public QEntry<K, V> before; //當前節點的前一個節點
public QEntry<K, V> after; //當前節點的后一個節點
QEntry(K key, V value, int hash, QEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
//刪除當前節點
private void remove() {
//當前節點的上一個節點的after指向當前節點的下一個節點
this.before.after = after;
//當前節點的下一個節點的before指向當前節點的上一個節點
this.after.before = before;
}
//將當前節點插入到existingEntry節點之前
private void addBefore(QEntry<K, V> existingEntry) {
//插入到existingEntry前,那么當前節點后一個節點指向existingEntry
this.after = existingEntry;
//當前節點的上一個節點也需要指向existingEntry節點的上一個節點
this.before = existingEntry.before;
//當前節點的下一個節點的before也得指向自己
this.after.before = this;
//當前節點的上一個節點的after也得指向自己
this.before.after = this;
}
//訪問了當前節點時,會調用這個函數
//在這里面就會處理訪問順序和插入順序
void recordAccess(QLinkedHashMap<K, V> m) {
QLinkedHashMap<K, V> lm = (QLinkedHashMap<K, V>) m;
//如果accessOrder為true,也就是訪問順序
if (lm.accessOrder) {
//把當前節點從鏈表中刪除
remove();
//再把當前節點插入到雙向鏈表的尾部
addBefore(lm.header);
}
}
}
}
上面就是QLinkedHashMap的全部源碼。
擴容以及刪除功能我們沒有寫,有興趣的讀者可以自己實現一下。
我們寫一段代碼來測試,如下:
public static void main(String[] args){
//新建一個默認的構造函數,默認是按照插入順序保存
QLinkedHashMap<String,String> map = new QLinkedHashMap<>();
map.put("name","tom");
map.put("age","32");
map.put("address","beijing");
//驗證是不是按照插入的順序打印
QLinkedHashMap.QIterator iterator = map.iterator();
while (iterator.hasNext()){
QEntry e = iterator.next();
System.out.println("key=" + e.key + " value=" + e.value);
}
}
輸出如下:
key=name value=tom
key=age value=32
key=address value=beijing
可以看到我們輸出的時候,是按照插入的順序輸出的。
我們再稍微改一下代碼,只需要改QLinkedHashMap的構造函數即可
代碼如下:
public static void main(String[] args){
//新建一個大小為16,順序是訪問順序的 map
QLinkedHashMap<String,String> map = new QLinkedHashMap<>(16,true);
//分別插入以下鍵值對
map.put("name","tom");
map.put("age","32");
map.put("address","beijing");
//訪問其中一個元素,這里什么也不做
//訪問了age,那么打印的時候,age應該是最后一個打印的
map.get("age");
//驗證是不是按照訪問順序打印,age是不是最后一個打印
QLinkedHashMap.QIterator iterator = map.iterator();
while (iterator.hasNext()){
QEntry e = iterator.next();
System.out.println("key=" + e.key + " value=" + e.value);
}
}
輸出如下:
key=name value=tom
key=address value=beijing
key=age value=32
由上可以,我們實現了可以以插入順序和訪問順序的HashMap。
雖然沒有實現擴容機制和刪除。但足以提示QLinkedHashMap的核心原理。