LinkedList,LinkedHashMap,LruCache原始碼解析
最近正好在複習資料結構的知識,順帶看了下jdk 1.8中的LinkedList和LinkedHashMap以及android中常用的LruCache的原始碼(內部採用LinkedHashMap實現),以加強自己的理解,下面就分享一下我閱讀原始碼的一些簡單的心得。
一、簡單高效的雙鏈表LinkedList
為什麼使用雙鏈表而不使用單鏈表,原因應該是,作為一種需要頻繁在表頭或表尾進行插入或刪除操作的資料結構,選用雙鏈表的效率會比單鏈表要高。試想一下,如果要刪除單鏈表的表尾節點,除了需要將最後一個節點置空,還需要將該節點的上一個節點的next域置為null,因為此時無法直接通過最後一個節點得到倒數第二個節點的位置,所以只能重新從表頭開始遍歷,時間複雜度為O(n),而如果是雙鏈表的話,可以直接通過最後一個節點的prev域即前驅節點得到它上一個節點,然後再將其next域置空,時間複雜度為O(1)。所以,雙鏈表的優勢就是,增加或刪除節點的速度較快,尤其是在表尾節點。
原始碼
先來看下節點類的定義
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
很簡單,Node類是一個靜態內部類,包含了資料部分和兩個引用,分別指向前驅節點和後繼節點,在節點類構造的時候分別指定它的前驅節點,資料域和後繼節點。這樣的建構函式,在後面進行插入或刪除操作的時候給我們省去了很多麻煩。
在表頭插入
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
我們來分析一下,也不是很複雜,first是LinkedList的成員變數,即連結串列的頭指標,該方法首先先儲存原來的頭結點,賦值給一個新的Node類f,然後建立新的節點,資料為e,前驅節點為null(因為要做新的第一個嘛),後繼節點是f,接著將頭指標指向新建立的節點。這樣就成功地將新節點插入到了原頭結點的前面,但由於是雙鏈表,插入刪除時需要調整兩部分的指標,我們還要將原來頭結點(f)的prev域設為新的頭結點,在這之前,先判斷一下原來的頭結點是不是為null,如果為null的話,說明原來的雙鏈表是空表,現在插入的是第一個節點,所以last尾指標也設為newNode。最後增加連結串列的size。
在表尾插入
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
與在表頭插入很類似,先儲存原先尾節點的值為l,然後建立新的尾節點插入到l後面,然後調整原先尾節點l的next域,指向新的尾節點。在這之前同樣先判斷一下是不是空表,是空表的話,插入一個節點後first頭結點也指向newNode。
在一個節點之前插入
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
該方法將一個新節點插入到succ節點前面。邏輯也很清晰,首先先獲取到succ的前驅節點pred,然後新建立一個節點插入到pred和succ兩者之間,然後分別修改succ的prev域和pred的next域,都指向新的節點。同樣的,在修改pred的next域之前,判斷pred是否為空,如果為空,說明原來succ是頭結點,所以要把頭指標指向新建立的節點newNode。
在表頭刪除
/**
* Unlinks non-null first node f.
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
在刪除表頭節點f的時候,先儲存其下一個節點next,接著將f的資料域和指標域強制置為null,這樣可以幫助垃圾收集器GC很快的回收這兩個引用。跟著將新的頭指標指向next,然後判斷next是否為空,如果next為空,說明原來只有一個節點,刪除後表變空了,所以將last也設為null,否則的話將next(此時的新頭結點)的prev前驅指標設為null,最後修改表的長度大小,並將刪除的頭結點的值返回。
在表尾刪除
/**
* Unlinks non-null last node l.
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
邏輯與上面在表頭刪除正好相反,就不在贅述了。
在表中刪除
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
從雙鏈表中刪除指定結點x,首先獲得x的前驅和後繼節點,如果前驅節點為null,說明x為頭結點,刪除後直接將頭指標指向x的後繼節點,如果不是頭結點則將x的前驅節點的後繼指向x的後繼,並將x的前驅置為空,將x從鏈中斷開,此處畫個圖就很好理解;接著同樣判斷x的後繼next是不是空,如果是空說明x是尾節點,要刪除的話則直接將尾指標指向x的前驅,否則修改x後繼節點的前驅指標,指向x的前驅,再把x的next置為null,將x從鏈中斷開。
我們常用的一些add和remove操作,呼叫的都是上面的函式。
public boolean add(E e) {
linkLast(e);
return true;
}
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
如果你理解了雙鏈表的基本插入刪除操作,那麼LinkedList的原始碼你也可以差不多基本理解了,剩下的一些細節我就不再說了,下面看LinkedHashMap。
二、LinkedHashMap
LinkedHashMap是HashMap的子類,通俗的講就是加了雙鏈表結構的HashMap。HashMap大家都很清楚,本質就是Entry陣列加連結串列(或者紅黑樹)的形式,Entry這個資料結構包括hash值,key-value鍵值對,和next索引(通過鏈地址法用來解決雜湊衝突)。而我們看看LinkedHashMap裡的Entry
LinkedHashMap的Entry
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);
}
}
可以看到LinkedHashMap在原來HashMap的Entry的基礎上又增加了before和after兩個指標(java中只有引用,這裡說指標是為了方便理解),分別指向前驅和後繼節點,所以說,它是一個完完全全的雙鏈表+HashMap。
雙鏈表表頭與表尾的定義
/**
* 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
final boolean accessOrder;
如果accessOrder為true表明LinkedHashMap按照訪問的順序來迭代,如果為false表明LinkedHashMap按照插入的順序來迭代。預設是按照插入順序來遍歷:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
在建立新節點的時候,是直接將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;
}
下面我們就來看一下這個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;
}
}
linkNodeLast方法將一個Entry節點加入到雙鏈表的尾部。首先儲存原雙鏈表的表尾節點tail為last,然後將tail指向新插入的節點p,此時判斷原來的表尾節點last是否為null,如果為null,說明原來雙鏈表為空表,插入後只有一個節點,所以將head頭指標也指向p,否則的話,將p的前驅指向原來的表尾節點last,將原來表尾節點last的後繼指向新的表尾節點p。
細想一下,和LinkedList那段是不是很像?沒錯,因為歸根結底還是雙鏈表的插入操作。
下面看一下LinkedHashMap的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;
}
可以看到,在訪問完一個節點後,如果accessOrder為true的話(即設定按照訪問順序來迭代),會呼叫afterNodeAccess()函式將剛訪問過的節點放置到雙鏈表的尾部,即放到最新的位置,代表這個節點剛被訪問過。我們再去afterNodeAccess()函式看看究竟
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) //如果p是表頭
head = a; //將e從表中斷開後,表頭指向e的後繼
else
b.after = a; //否則將e從表中斷開
if (a != null)
a.before = b;
else //p是表尾
last = b; //將e從表中斷開後,表尾指向e的前驅
if (last == null) //如果原連結串列是空表
head = p; //表頭指向p
else { //否則插向原連結串列的表尾
p.before = last;
last.after = p;
}
tail = p; //尾節點指向新插入的節點
++modCount;
}
}
該函式將節點e從原雙鏈表中摘下,並插入到最後的位置。這裡還是先用一個last來儲存原來的tail表尾節點,如果accessOrder為true,並且此時節點p(由e轉換而來)並不在表尾,則執行後面的操作,後面的操作可以分為兩部分,第一部分是將節點p從原來雙鏈表的位置中斷開,第二部分是將節點p插入到表尾。和之前在LinkedList中的操作很類似,將節點p從原連結串列中刪除時,判斷了p是否在表頭或是在表尾(與LinkedList的unlinkFirst和unlinkLast函式相同);將p插入到連結串列尾部時,加入了表是否為空的判斷(與LinkedList的linkLast函式相同)。
綜上,我們可以看到,將LinkedList中雙鏈表的增加和刪除操作與HashMap相結合,就是LinkedHashMap。
三、LruCache
理解了LinkedHashMap,就不難理解LruCache的實現原理了。這個Android中最常用的快取類,內部就維護了一個LinkedHashMap的引用
private final LinkedHashMap<K, V> map;
在 LruCache初始化時,指定了hasmap的擴容因子,並設定accessOrder為true,按訪問順序迭代,來達到LRU(最近最久未使用)演算法的效果:最近被訪問的,或者最新插入的,總是在表尾,而不怎麼被經常訪問的,就會逐漸向表頭移動,此時就可以從表頭將這些不常用的快取淘汰。
我們來看一下從快取中取資料的get方法
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// 如果根據相應的key能查詢到value,就增加一次快取命中的次數hitCount,並且返回結果
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 否則增加一次未命中次數missCount
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
// 如果我們重寫了create(key)方法而且返回值不為空,那麼將上述的key與這個返回值寫入到map當中
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
// 方法放入最後put的key,value值
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
// 這個方法也可以重寫
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
下面再看一下LruCache更新快取的策略,主要在trimToSize()這個函式中
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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 || map.isEmpty()) {
break;
}
//按照訪問順序來迭代,最新訪問過的都在表尾,表頭的是最近長時間內都沒有使用過的快取
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
//將快取移除
map.remove(key);
//修改快取連結串列的size
size -= safeSizeOf(key, value);
//淘汰掉一個快取
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
從註釋中就可以看到,這個函式將最老的也就是最久沒有訪問過的entry刪除,以將整體entry的容量降低到指定大小(淘汰了最近不常用的快取)。可以看到,它通過map.entrySet().iterator()獲得LinkedHashMap的迭代器,從儲存了快取資料的雙鏈表的第一個節點開始(即最久沒有使用過的快取,因為最近剛使用過的快取都移到了表尾),逐個呼叫remove函式,將其從表中刪除,以降低整體快取的大小。
看到了這裡,是不是對LinkedList,LinkedHashMap,LruCache的基本原理有了一個清楚的認識呢?我們看到,萬變不離其宗,其重點就是圍繞雙鏈表的增刪改操作,資料結構的基礎確實很重要。
相關推薦
LinkedList,LinkedHashMap,LruCache原始碼解析
最近正好在複習資料結構的知識,順帶看了下jdk 1.8中的LinkedList和LinkedHashMap以及android中常用的LruCache的原始碼(內部採用LinkedHashMap實現),以加強自己的理解,下面就分享一下我閱讀原始碼的一些簡單的心得。
LRU演算法,以及Apache LRUMap原始碼解析
1. 什麼是LRU LRU(least recently used) : 最近最少使用 LRU就是一種經典的演算法,在容器中,對元素定義一個最後使用時間,當新的元素寫入的時候,如果容器已滿,則淘汰最近最少使用的元素,把新的元素寫入。 1.1 自定義實現LRU的要求 比
2018.9.30學習筆記(Map,HashMap,LinkedHashMap,TreeMap)
1 Map 單列集合底層是雙列集合 2 Map的基本方法 package com.haidai.Map; import java.util.HashMap; import java.ut
HashMap,LinkedHashMap,TreeMap區別
注:去年專案有用到LinkedHashMap,最開始忘記這個,浪費了點時間,剛好看到阿里開發手冊,記錄一下區別。 HashMap 最多隻允許一條記錄的鍵為Null;允許多條記錄的值為 Null 非執行緒安全 LinkedHashMap 儲存了記錄的插入
Android——LruCache原始碼解析
以下針對 Android API 26 版本的原始碼進行分析。 在瞭解LruCache之前,最好對LinkedHashMap有初步的瞭解,LruCache的實現主要藉助LinkedHashMap。LinkedHashMap的原始碼解析,可閱讀Java——LinkedHashMap原始碼解析 概述 &nbs
LruCache原始碼解析
LruCache 之前分析過Lru演算法的實現方式:HashMap+雙向連結串列,參考連結:LRU演算法&&LeetCode解題報告 這裡主要介紹Android SDK中LruCache快取演算法的實現, 基於Android5.1版本原始碼.
集合,ArrayList,LinkedList,HashMap,LinkedHashMap,ConcurremtHashMap分別的總結,volatile 關鍵字的使用
1 集合 1.1 List 1.1.1 ArrayList 動態陣列 實現list介面,非同步 &nbs
==和equals的區別,原始碼解析
這是Object類裡面的equals方法原始碼: public boolean equals(Object obj) { return (this == obj); } 看著還是用==在比較,原因分析:在大部分的封裝類中,都重寫了Object類的這個方法,
fastjson 始終將 null 物件以 "null " 的形式返回到前端引發的原始碼解析 - 下:來到 fasjson 內部,消除疑惑
接上篇:fastjson 始終將 null 物件以 "null " 的形式返回到前端引發的原始碼解析 - 上:從 DispatcherServlet 出發 終於來到了 fastjson 內部。 FastJsonHttpMessageConverter 內部又呼叫了一系列的方法
原始碼解析關於java阻塞容器:ArrayBlockingQueue,LinkedBlockingQueue等
Java的阻塞容器介紹JDK18 一ArrayBlockingQueue 類的定義 重要的成員變數 初始化 一些重要的非公開的方法
java 集合類 底層原始碼解析,慢速更新~偏新手
我決定從java底層原始碼開始自己的部落格之旅,水平有限,很有可能寫的不對,歡迎大家指出缺點~部落格慢速保持更新! 先從java最常用的集合類開始更新吧~ java的集合類均來自於 java.util包下 java單列頂層介面 Collection 先看看該介面的定義: pub
LeetCode題目,原始碼解析
leetcode_java 前言 刷leetcode的記錄,程式碼並不是十分滴優秀~~網上優秀滴題解也非常多…嗯…但還是想放上來,給自己看!嗯…很調皮! 介紹 大多題目的解題思路在程式碼中都有註釋,點選語言一欄中的Java, 跳轉到相對應的程式碼,github會實時更新,cs
原始碼系列Spring,Mybatis,Springboot,Netty原始碼深度解析-Spring的整體架構與容器的基本實現-mybatis原始碼深度解析與最佳實踐
6套原始碼系列Spring,Mybatis,Springboot,Netty原始碼深度解析視訊課程 6套原始碼套餐課程介紹: 1、6套精品是掌櫃最近整理出的最新課程,都是當下最火的技術,最火的課程,也是全網課程的精品; 2、6套資源包含:全套完整
mybatis原始碼-解析配置檔案(三)之配置檔案Configuration解析(超詳細, 值得收藏)
1. 簡介 1.1 系列內容 本系列文章講解的是mybatis解析配置檔案內部的邏輯, 即 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFact
Laravel原始碼解析之入口,程式設計師必學
前言 提升能力的方法並非使用更多工具,而是解刨自己所使用的工具。今天我們從Laravel啟動的第一步開始講起。 入口檔案 laravel是單入口框架,所有請求必將經過index.php define(‘LARAVEL_START’, microtime(true
Spring的IOC容器原始碼學習----Bean的定位,載入,解析,註冊,注入
目錄 IOC容器系列包含BeanFactory和ApplicationContext,這兩個介面就是IOC的具體表現形式。 他們的介面關係設計圖如下所示: 主要介面設計主線: 1.BeanFactory -> Hierarc
JDK8 HashMap原始碼解析,一篇文章徹底讀懂HashMap
在秋招面試準備中博主找過很多關於HashMap的部落格,但是秋招結束後回過頭來看,感覺沒有一篇全面、通俗易懂的講解HashMap文章(可能是博主沒有找到),所以在秋招結束後,寫下了這篇文章,盡最大的努力把HashMap原始碼講解的通俗易懂,並且儘量涵蓋面試中HashM
JAVA架構師VIP精品教程,Spring原始碼解析視訊
JAVA架構師VIP精品課程 ,某泡學院稀缺資源, 《重磅乾貨Java架構師全套vip課程視訊教程》2018年最新視訊教程 java架構師篇,從學習計劃到java原始碼分析,
jdk8原始碼解析第一天(簡化版,只記自己理解的要點)
1.String類: 實現了serilizable,comparable介面,seriliazable僅用於標誌,comparable的comparableTo方法用於比較字串大小。 底層是通過final char[] 實現字串的,其所有方法均是用字元陣列相關方法實現的。
Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(上)-郭霖
其實我一直準備寫一篇關於Android事件分發機制的文章,從我的第一篇部落格開始,就零零散散在好多地方使用到了Android事件分發的知識。也有好多朋友問過我各種問題,比如:onTouch和onTouchEvent有什麼區別,又該如何使用?為什麼給ListView引入了一