6 手寫Java LinkedHashMap 核心原始碼
概述
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的核心原理。