Java集合詳解5:深入理解LinkedHashMap和LRU緩存

《Java集合詳解系列》是我在完成夯實(shí)Java基礎(chǔ)篇的系列博客后準(zhǔn)備開始寫的新系列。

這些文章將整理到我在GitHub上的《Java面試指南》倉庫吏奸,更多精彩內(nèi)容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點(diǎn)下Star黎休、fork哈

文章首發(fā)于我的個(gè)人博客:

www.how2playlife.com

今天我們來深入探索一下LinkedHashMap的底層原理浓领,并且使用linkedhashmap來實(shí)現(xiàn)LRU緩存。

摘要:

HashMap和雙向鏈表合二為一即是LinkedHashMap势腮。所謂LinkedHashMap联贩,其落腳點(diǎn)在HashMap,因此更準(zhǔn)確地說捎拯,它是一個(gè)將所有Entry節(jié)點(diǎn)鏈入一個(gè)雙向鏈表的HashMap泪幌。

由于LinkedHashMap是HashMap的子類,所以LinkedHashMap自然會擁有HashMap的所有特性。比如祸泪,LinkedHashMap的元素存取過程基本與HashMap基本類似吗浩,只是在細(xì)節(jié)實(shí)現(xiàn)上稍有不同。當(dāng)然没隘,這是由LinkedHashMap本身的特性所決定的懂扼,因?yàn)樗~外維護(hù)了一個(gè)雙向鏈表用于保持迭代順序。

此外右蒲,LinkedHashMap可以很好的支持LRU算法阀湿,筆者在第七節(jié)便在LinkedHashMap的基礎(chǔ)上實(shí)現(xiàn)了一個(gè)能夠很好支持LRU的結(jié)構(gòu)。

友情提示:

本文所有關(guān)于 LinkedHashMap 的源碼都是基于 JDK 1.6 的瑰妄,不同 JDK 版本之間也許會有些許差異陷嘴,但不影響我們對 LinkedHashMap 的數(shù)據(jù)結(jié)構(gòu)、原理等整體的把握和了解间坐。后面會講解1.8對于LinkedHashMap的改動(dòng)灾挨。

由于 LinkedHashMap 是 HashMap 的子類,所以其具有HashMap的所有特性竹宋,這一點(diǎn)在源碼共用上體現(xiàn)的尤為突出劳澄。因此,讀者在閱讀本文之前逝撬,最好對 HashMap 有一個(gè)較為深入的了解和回顧浴骂,否則很可能會導(dǎo)致事倍功半∠艹保可以參考我之前關(guān)于hashmap的文章溯警。

LinkedHashMap 概述

筆者曾提到,HashMap 是 Java Collection Framework 的重要成員狡相,也是Map族(如下圖所示)中我們最為常用的一種梯轻。不過遺憾的是,HashMap是無序的尽棕,也就是說喳挑,迭代HashMap所得到的元素順序并不是它們最初放置到HashMap的順序。

HashMap的這一缺點(diǎn)往往會造成諸多不便滔悉,因?yàn)樵谟行﹫鼍爸幸了校覀兇_需要用到一個(gè)可以保持插入順序的Map。慶幸的是回官,JDK為我們解決了這個(gè)問題曹宴,它為HashMap提供了一個(gè)子類 —— LinkedHashMap。雖然LinkedHashMap增加了時(shí)間和空間上的開銷歉提,但是它通過維護(hù)一個(gè)額外的雙向鏈表保證了迭代順序笛坦。

特別地区转,該迭代順序可以是插入順序,也可以是訪問順序版扩。因此废离,根據(jù)鏈表中元素的順序可以將LinkedHashMap分為:保持插入順序的LinkedHashMap和保持訪問順序的LinkedHashMap,其中LinkedHashMap的默認(rèn)實(shí)現(xiàn)是按插入順序排序的礁芦。

image

本質(zhì)上蜻韭,HashMap和雙向鏈表合二為一即是LinkedHashMap。所謂LinkedHashMap宴偿,其落腳點(diǎn)在HashMap湘捎,因此更準(zhǔn)確地說,它是一個(gè)將所有Entry節(jié)點(diǎn)鏈入一個(gè)雙向鏈表雙向鏈表的HashMap窄刘。

在LinkedHashMapMap中,所有put進(jìn)來的Entry都保存在如下面第一個(gè)圖所示的哈希表中舷胜,但由于它又額外定義了一個(gè)以head為頭結(jié)點(diǎn)的雙向鏈表(如下面第二個(gè)圖所示)娩践,因此對于每次put進(jìn)來Entry,除了將其保存到哈希表中對應(yīng)的位置上之外烹骨,還會將其插入到雙向鏈表的尾部翻伺。

image

更直觀地,下圖很好地還原了LinkedHashMap的原貌:HashMap和雙向鏈表的密切配合和分工合作造就了LinkedHashMap沮焕。特別需要注意的是吨岭,next用于維護(hù)HashMap各個(gè)桶中的Entry鏈,before峦树、after用于維護(hù)LinkedHashMap的雙向鏈表辣辫,雖然它們的作用對象都是Entry,但是各自分離魁巩,是兩碼事兒急灭。

這里寫圖片描述

  
  其中,HashMap與LinkedHashMap的Entry結(jié)構(gòu)示意圖如下圖所示:

這里寫圖片描述

特別地谷遂,由于LinkedHashMap是HashMap的子類葬馋,所以LinkedHashMap自然會擁有HashMap的所有特性。比如肾扰,==LinkedHashMap也最多只允許一條Entry的鍵為Null(多條會覆蓋)畴嘶,但允許多條Entry的值為Null。==
  
  此外集晚,LinkedHashMap 也是 Map 的一個(gè)非同步的實(shí)現(xiàn)窗悯。此外,LinkedHashMap還可以用來實(shí)現(xiàn)LRU (Least recently used, 最近最少使用)算法甩恼,這個(gè)問題會在下文的特別談到蟀瞧。

LinkedHashMap 在 JDK 中的定義

類結(jié)構(gòu)定義

LinkedHashMap繼承于HashMap沉颂,其在JDK中的定義為:

public class LinkedHashMap<K,V> extends HashMap<K,V>
    implements Map<K,V> {

    ...
}

成員變量定義

與HashMap相比,LinkedHashMap增加了兩個(gè)屬性用于保證迭代順序悦污,分別是 雙向鏈表頭結(jié)點(diǎn)header 和 標(biāo)志位accessOrder (值為true時(shí)铸屉,表示按照訪問順序迭代;值為false時(shí)切端,表示按照插入順序迭代)彻坛。

/**
 * The head of the doubly linked list.
 */
private transient Entry<K,V> header;  // 雙向鏈表的表頭元素

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
private final boolean accessOrder;  //true表示按照訪問順序迭代,false時(shí)表示按照插入順序 

成員方法定義

從下圖我們可以看出踏枣,LinkedHashMap中并增加沒有額外方法昌屉。也就是說,LinkedHashMap與HashMap在操作上大致相同茵瀑,只是在實(shí)現(xiàn)細(xì)節(jié)上略有不同罷了间驮。

[外鏈圖片轉(zhuǎn)存失敗(img-C2vYmjQ7-1567839753833)(http://static.zybuluo.com/Rico123/nvojgv4s0o0ciieibz1tbakc/LinkedHashMap_Outline.png)]

基本元素 Entry

LinkedHashMap采用的hash算法和HashMap相同,但是它重新定義了Entry马昨。LinkedHashMap中的Entry增加了兩個(gè)指針 before 和 after竞帽,它們分別用于維護(hù)雙向鏈接列表。特別需要注意的是鸿捧,next用于維護(hù)HashMap各個(gè)桶中Entry的連接順序屹篓,before、after用于維護(hù)Entry插入的先后順序的匙奴,源代碼如下:

private static class Entry<K,V> extends HashMap.Entry<K,V> {

    // These fields comprise the doubly linked list used for iteration.
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
        super(hash, key, value, next);
    }
    ...
}

形象地堆巧,HashMap與LinkedHashMap的Entry結(jié)構(gòu)示意圖如下圖所示:

這里寫圖片描述

LinkedHashMap 的構(gòu)造函數(shù)

LinkedHashMap 一共提供了五個(gè)構(gòu)造函數(shù),它們都是在HashMap的構(gòu)造函數(shù)的基礎(chǔ)上實(shí)現(xiàn)的泼菌,除了默認(rèn)空參數(shù)構(gòu)造方法谍肤,下面這個(gè)構(gòu)造函數(shù)包含了大部分其他構(gòu)造方法使用的參數(shù),就不一一列舉了灶轰。

LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

該構(gòu)造函數(shù)意在構(gòu)造一個(gè)指定初始容量和指定負(fù)載因子的具有指定迭代順序的LinkedHashMap谣沸,其源碼如下:

/**
* 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); // 調(diào)用HashMap對應(yīng)的構(gòu)造函數(shù)
this.accessOrder = accessOrder; // 迭代順序的默認(rèn)值
}

初始容量 和負(fù)載因子是影響HashMap性能的兩個(gè)重要參數(shù)。同樣地笋颤,它們也是影響LinkedHashMap性能的兩個(gè)重要參數(shù)乳附。此外,LinkedHashMap 增加了雙向鏈表頭結(jié)點(diǎn) header和標(biāo)志位 accessOrder兩個(gè)屬性用于保證迭代順序伴澄。

LinkedHashMap(Map<? extends K, ? extends V> m)

該構(gòu)造函數(shù)意在構(gòu)造一個(gè)與指定 Map 具有相同映射的 LinkedHashMap赋除,其 初始容量不小于 16 (具體依賴于指定Map的大小),負(fù)載因子是 0.75非凌,是 Java Collection Framework 規(guī)范推薦提供的举农,其源碼如下:

/**
 * 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(m);       // 調(diào)用HashMap對應(yīng)的構(gòu)造函數(shù)
    accessOrder = false;    // 迭代順序的默認(rèn)值
}

init 方法

從上面的五種構(gòu)造函數(shù)我們可以看出,無論采用何種方式創(chuàng)建LinkedHashMap敞嗡,其都會調(diào)用HashMap相應(yīng)的構(gòu)造函數(shù)颁糟。事實(shí)上航背,不管調(diào)用HashMap的哪個(gè)構(gòu)造函數(shù),HashMap的構(gòu)造函數(shù)都會在最后調(diào)用一個(gè)init()方法進(jìn)行初始化棱貌,只不過這個(gè)方法在HashMap中是一個(gè)空實(shí)現(xiàn)玖媚,而在LinkedHashMap中重寫了它用于初始化它所維護(hù)的雙向鏈表。例如婚脱,HashMap的參數(shù)為空的構(gòu)造函數(shù)以及init方法的源碼如下:

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

/**
* Initialization hook for subclasses. This method is called
* in all constructors and pseudo-constructors (clone, readObject)
* after HashMap has been initialized but before any entries have
* been inserted. (In the absence of this method, readObject would
* require explicit knowledge of subclasses.)
*/
void init() {
}

在LinkedHashMap中今魔,它重寫了init方法以便初始化雙向列表,源碼如下:

/**
* Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}

因此障贸,我們在創(chuàng)建LinkedHashMap的同時(shí)就會不知不覺地對雙向鏈表進(jìn)行初始化错森。

LinkedHashMap 的數(shù)據(jù)結(jié)構(gòu)

本質(zhì)上,LinkedHashMap = HashMap + 雙向鏈表篮洁,也就是說涩维,HashMap和雙向鏈表合二為一即是LinkedHashMap。

也可以這樣理解嘀粱,LinkedHashMap 在不對HashMap做任何改變的基礎(chǔ)上激挪,給HashMap的任意兩個(gè)節(jié)點(diǎn)間加了兩條連線(before指針和after指針),使這些節(jié)點(diǎn)形成一個(gè)雙向鏈表锋叨。

在LinkedHashMapMap中,所有put進(jìn)來的Entry都保存在HashMap中宛篇,但由于它又額外定義了一個(gè)以head為頭結(jié)點(diǎn)的空的雙向鏈表娃磺,因此對于每次put進(jìn)來Entry還會將其插入到雙向鏈表的尾部。

這里寫圖片描述

LinkedHashMap 的快速存取

我們知道叫倍,在HashMap中最常用的兩個(gè)操作就是:put(Key,Value) 和 get(Key)偷卧。同樣地,在 LinkedHashMap 中最常用的也是這兩個(gè)操作吆倦。

對于put(Key,Value)方法而言听诸,LinkedHashMap完全繼承了HashMap的 put(Key,Value) 方法,只是對put(Key,Value)方法所調(diào)用的recordAccess方法和addEntry方法進(jìn)行了重寫蚕泽;對于get(Key)方法而言晌梨,LinkedHashMap則直接對它進(jìn)行了重寫。

下面我們結(jié)合JDK源碼看 LinkedHashMap 的存取實(shí)現(xiàn)须妻。

LinkedHashMap 的存儲實(shí)現(xiàn) : put(key, vlaue)

上面談到仔蝌,LinkedHashMap沒有對 put(key,vlaue) 方法進(jìn)行任何直接的修改,完全繼承了HashMap的 put(Key,Value) 方法荒吏,其源碼如下:

public V put(K key, V value) {

    //當(dāng)key為null時(shí)敛惊,調(diào)用putForNullKey方法,并將該鍵值對保存到table的第一個(gè)位置 
    if (key == null)
        return putForNullKey(value); 

    //根據(jù)key的hashCode計(jì)算hash值
    int hash = hash(key.hashCode());           

    //計(jì)算該鍵值對在數(shù)組中的存儲位置(哪個(gè)桶)
    int i = indexFor(hash, table.length);              

    //在table的第i個(gè)桶上進(jìn)行迭代绰更,尋找 key 保存的位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;
        //判斷該條鏈上是否存在hash值相同且key值相等的映射瞧挤,若存在锡宋,則直接覆蓋 value,并返回舊value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this); // LinkedHashMap重寫了Entry中的recordAccess方法--- (1)    
            return oldValue;    // 返回舊值
        }
    }

    modCount++; //修改次數(shù)增加1特恬,快速失敗機(jī)制

    //原Map中無該映射执俩,將該添加至該鏈的鏈頭
    addEntry(hash, key, value, i);  // LinkedHashMap重寫了HashMap中的createEntry方法 ---- (2)    
    return null;
}

上述源碼反映了LinkedHashMap與HashMap保存數(shù)據(jù)的過程。特別地鸵鸥,在LinkedHashMap中奠滑,它對addEntry方法和Entry的recordAccess方法進(jìn)行了重寫。下面我們對比地看一下LinkedHashMap 和HashMap的addEntry方法的具體實(shí)現(xiàn):

/**
 * This override alters behavior of superclass put method. It causes newly
 * allocated entry to get inserted at the end of the linked list and
 * removes the eldest entry if appropriate.
 *
 * LinkedHashMap中的addEntry方法
 */
void addEntry(int hash, K key, V value, int bucketIndex) {   

    //創(chuàng)建新的Entry妒穴,并插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重寫了HashMap中的createEntry方法

    //雙向鏈表的第一個(gè)有效節(jié)點(diǎn)(header后的那個(gè)節(jié)點(diǎn))為最近最少使用的節(jié)點(diǎn)宋税,這是用來支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //如果有必要,則刪除掉該近期最少使用的節(jié)點(diǎn)讼油,  
    //這要看對removeEldestEntry的覆寫,由于默認(rèn)為false杰赛,因此默認(rèn)是不做任何處理的。  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        //擴(kuò)容到原來的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
} 

-------------------------------我是分割線------------------------------------

 /**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 * 
 * HashMap中的addEntry方法
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    //獲取bucketIndex處的Entry
    Entry<K,V> e = table[bucketIndex];

    //將新創(chuàng)建的 Entry 放入 bucketIndex 索引處矮台,并讓新的 Entry 指向原來的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

    //若HashMap中元素的個(gè)數(shù)超過極限了乏屯,則容量擴(kuò)大兩倍
    if (size++ >= threshold)
        resize(2 * table.length);
}

由于LinkedHashMap本身維護(hù)了插入的先后順序,因此其可以用來做緩存瘦赫,14~19行的操作就是用來支持LRU算法的辰晕,這里暫時(shí)不用去關(guān)心它。此外确虱,在LinkedHashMap的addEntry方法中含友,它重寫了HashMap中的createEntry方法,我們接著看一下createEntry方法:

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry校辩,這點(diǎn)與HashMap中相同 
    //創(chuàng)建新的Entry并將其鏈入到數(shù)組對應(yīng)桶的鏈表的頭結(jié)點(diǎn)處窘问, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同時(shí),都會將其插入到雙向鏈表的尾部宜咒,  
    //這樣就按照Entry插入LinkedHashMap的先后順序來迭代元素(LinkedHashMap根據(jù)雙向鏈表重寫了迭代器)
    //同時(shí)惠赫,新put進(jìn)來的Entry是最近訪問的Entry,把其放在鏈表末尾 故黑,也符合LRU算法的實(shí)現(xiàn)  
    e.addBefore(header);  
    size++;  
}  

由以上源碼我們可以知道儿咱,在LinkedHashMap中向哈希表中插入新Entry的同時(shí),還會通過Entry的addBefore方法將其鏈入到雙向鏈表中倍阐。其中概疆,addBefore方法本質(zhì)上是一個(gè)雙向鏈表的插入操作,其源碼如下:

//在雙向鏈表中峰搪,將當(dāng)前的Entry插入到existingEntry(header)的前面  
private void addBefore(Entry<K,V> existingEntry) {  
    after  = existingEntry;  
    before = existingEntry.before;  
    before.after = this;  
    after.before = this;  
}  

到此為止岔冀,我們分析了在LinkedHashMap中put一條鍵值對的完整過程。總的來說使套,相比HashMap而言罐呼,LinkedHashMap在向哈希表添加一個(gè)鍵值對的同時(shí),也會將其鏈入到它所維護(hù)的雙向鏈表中侦高,以便設(shè)定迭代順序嫉柴。

LinkedHashMap 的擴(kuò)容操作 : resize()

在HashMap中,我們知道隨著HashMap中元素的數(shù)量越來越多奉呛,發(fā)生碰撞的概率將越來越大计螺,所產(chǎn)生的子鏈長度就會越來越長,這樣勢必會影響HashMap的存取速度瞧壮。

為了保證HashMap的效率登馒,系統(tǒng)必須要在某個(gè)臨界點(diǎn)進(jìn)行擴(kuò)容處理,該臨界點(diǎn)就是HashMap中元素的數(shù)量在數(shù)值上等于threshold(table數(shù)組長度*加載因子)咆槽。

但是陈轿,不得不說,擴(kuò)容是一個(gè)非常耗時(shí)的過程秦忿,因?yàn)樗枰匦掠?jì)算這些元素在新table數(shù)組中的位置并進(jìn)行復(fù)制處理麦射。所以,如果我們能夠提前預(yù)知HashMap中元素的個(gè)數(shù)灯谣,那么在構(gòu)造HashMap時(shí)預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高HashMap的性能潜秋。

同樣的問題也存在于LinkedHashMap中,因?yàn)長inkedHashMap本來就是一個(gè)HashMap胎许,只是它還將所有Entry節(jié)點(diǎn)鏈入到了一個(gè)雙向鏈表中半等。LinkedHashMap完全繼承了HashMap的resize()方法,只是對它所調(diào)用的transfer方法進(jìn)行了重寫呐萨。我們先看resize()方法源碼:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;

    // 若 oldCapacity 已達(dá)到最大值,直接將 threshold 設(shè)為 Integer.MAX_VALUE
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;
        return;             // 直接返回
    }

    // 否則乒融,創(chuàng)建一個(gè)更大的數(shù)組
    Entry[] newTable = new Entry[newCapacity];

    //將每條Entry重新哈希到新的數(shù)組中
    transfer(newTable);  //LinkedHashMap對它所調(diào)用的transfer方法進(jìn)行了重寫

    table = newTable;
    threshold = (int)(newCapacity * loadFactor);  // 重新設(shè)定 threshold
}

從上面代碼中我們可以看出锈麸,Map擴(kuò)容操作的核心在于重哈希袖牙。所謂重哈希是指重新計(jì)算原HashMap中的元素在新table數(shù)組中的位置并進(jìn)行復(fù)制處理的過程。鑒于性能和LinkedHashMap自身特點(diǎn)的考量惨远,LinkedHashMap對重哈希過程(transfer方法)進(jìn)行了重寫,源碼如下:

/**
 * Transfers all entries to new table array.  This method is called
 * by superclass resize.  It is overridden for performance, as it is
 * faster to iterate using our linked list.
 */
void transfer(HashMap.Entry[] newTable) {
    int newCapacity = newTable.length;
    // 與HashMap相比话肖,借助于雙向鏈表的特點(diǎn)進(jìn)行重哈希使得代碼更加簡潔
    for (Entry<K,V> e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);   // 計(jì)算每個(gè)Entry所在的桶
        // 將其鏈入桶中的鏈表
        e.next = newTable[index];
        newTable[index] = e;   
    }
}

如上述源碼所示北秽,LinkedHashMap借助于自身維護(hù)的雙向鏈表輕松地實(shí)現(xiàn)了重哈希操作。

LinkedHashMap 的讀取實(shí)現(xiàn) :get(Object key)

相對于LinkedHashMap的存儲而言最筒,讀取就顯得比較簡單了贺氓。LinkedHashMap中重寫了HashMap中的get方法,源碼如下:

public V get(Object key) {
    // 根據(jù)key獲取對應(yīng)的Entry床蜘,若沒有這樣的Entry辙培,則返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在這樣的Entry蔑水,直接返回
        return null;
    e.recordAccess(this);
    return e.value;
}

/**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     * 
     * HashMap 中的方法
     *     
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

在LinkedHashMap的get方法中,通過HashMap中的getEntry方法獲取Entry對象扬蕊。注意這里的recordAccess方法搀别,如果鏈表中元素的排序規(guī)則是按照插入的先后順序排序的話,該方法什么也不做尾抑;如果鏈表中元素的排序規(guī)則是按照訪問的先后順序排序的話歇父,則將e移到鏈表的末尾處,筆者會在后文專門闡述這個(gè)問題再愈。

另外榜苫,同樣地,調(diào)用LinkedHashMap的get(Object key)方法后践磅,若返回值是 NULL单刁,則也存在如下兩種可能:

該 key 對應(yīng)的值就是 null;
HashMap 中不存在該 key。

LinkedHashMap 存取小結(jié)

LinkedHashMap的存取過程基本與HashMap基本類似府适,只是在細(xì)節(jié)實(shí)現(xiàn)上稍有不同羔飞,這是由LinkedHashMap本身的特性所決定的,因?yàn)樗~外維護(hù)一個(gè)雙向鏈表用于保持迭代順序檐春。

在put操作上逻淌,雖然LinkedHashMap完全繼承了HashMap的put操作,但是在細(xì)節(jié)上還是做了一定的調(diào)整疟暖,比如卡儒,在LinkedHashMap中向哈希表中插入新Entry的同時(shí),還會通過Entry的addBefore方法將其鏈入到雙向鏈表中俐巴。

在擴(kuò)容操作上骨望,雖然LinkedHashMap完全繼承了HashMap的resize操作,但是鑒于性能和LinkedHashMap自身特點(diǎn)的考量欣舵,LinkedHashMap對其中的重哈希過程(transfer方法)進(jìn)行了重寫擎鸠。在讀取操作上,LinkedHashMap中重寫了HashMap中的get方法缘圈,通過HashMap中的getEntry方法獲取Entry對象劣光。在此基礎(chǔ)上,進(jìn)一步獲取指定鍵對應(yīng)的值糟把。

LinkedHashMap 與 LRU(Least recently used绢涡,最近最少使用)算法

到此為止,我們已經(jīng)分析完了LinkedHashMap的存取實(shí)現(xiàn)遣疯,這與HashMap大體相同雄可。LinkedHashMap區(qū)別于HashMap最大的一個(gè)不同點(diǎn)是,前者是有序的,而后者是無序的滞项。為此狭归,LinkedHashMap增加了兩個(gè)屬性用于保證順序,分別是雙向鏈表頭結(jié)點(diǎn)header和標(biāo)志位accessOrder文判。

我們知道过椎,header是LinkedHashMap所維護(hù)的雙向鏈表的頭結(jié)點(diǎn),而accessOrder用于決定具體的迭代順序戏仓。實(shí)際上疚宇,accessOrder標(biāo)志位的作用可不像我們描述的這樣簡單,我們接下來仔細(xì)分析一波~

我們知道赏殃,當(dāng)accessOrder標(biāo)志位為true時(shí)敷待,表示雙向鏈表中的元素按照訪問的先后順序排列,可以看到仁热,雖然Entry插入鏈表的順序依然是按照其put到LinkedHashMap中的順序榜揖,但put和get方法均有調(diào)用recordAccess方法(put方法在key相同時(shí)會調(diào)用)。

recordAccess方法判斷accessOrder是否為true抗蠢,如果是举哟,則將當(dāng)前訪問的Entry(put進(jìn)來的Entry或get出來的Entry)移到雙向鏈表的尾部(key不相同時(shí),put新Entry時(shí)迅矛,會調(diào)用addEntry妨猩,它會調(diào)用createEntry,該方法同樣將新插入的元素放入到雙向鏈表的尾部秽褒,既符合插入的先后順序壶硅,又符合訪問的先后順序,因?yàn)檫@時(shí)該Entry也被訪問了)销斟;

當(dāng)標(biāo)志位accessOrder的值為false時(shí)庐椒,表示雙向鏈表中的元素按照Entry插入LinkedHashMap到中的先后順序排序,即每次put到LinkedHashMap中的Entry都放在雙向鏈表的尾部蚂踊,這樣遍歷雙向鏈表時(shí)扼睬,Entry的輸出順序便和插入的順序一致,這也是默認(rèn)的雙向鏈表的存儲順序悴势。

因此,當(dāng)標(biāo)志位accessOrder的值為false時(shí)措伐,雖然也會調(diào)用recordAccess方法特纤,但不做任何操作。

put操作與標(biāo)志位accessOrder

/ 將key/value添加到LinkedHashMap中      
public V put(K key, V value) {      
    // 若key為null侥加,則將該鍵值對添加到table[0]中捧存。      
    if (key == null)      
        return putForNullKey(value);      
    // 若key不為null,則計(jì)算該key的哈希值,然后將其添加到該哈希值對應(yīng)的鏈表中昔穴。      
    int hash = hash(key.hashCode());      
    int i = indexFor(hash, table.length);      
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;      
        // 若key對已經(jīng)存在镰官,則用新的value取代舊的value     
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {      
            V oldValue = e.value;      
            e.value = value;      
            e.recordAccess(this);      
            return oldValue;      
        }      
    }      

    // 若key不存在,則將key/value鍵值對添加到table中      
    modCount++;    
    //將key/value鍵值對添加到table[i]處    
    addEntry(hash, key, value, i);      
    return null;      
}      

從上述源碼我們可以看到吗货,當(dāng)要put進(jìn)來的Entry的key在哈希表中已經(jīng)在存在時(shí)泳唠,會調(diào)用Entry的recordAccess方法;當(dāng)該key不存在時(shí)宙搬,則會調(diào)用addEntry方法將新的Entry插入到對應(yīng)桶的單鏈表的頭部笨腥。我們先來看recordAccess方法:

/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {  
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    //如果鏈表中元素按照訪問順序排序,則將當(dāng)前訪問的Entry移到雙向循環(huán)鏈表的尾部勇垛,  
    //如果是按照插入的先后順序排序脖母,則不做任何事情。  
    if (lm.accessOrder) {  
        lm.modCount++;  
        //移除當(dāng)前訪問的Entry  
        remove();  
        //將當(dāng)前訪問的Entry插入到鏈表的尾部  
        addBefore(lm.header);  
      }  
  } 

LinkedHashMap重寫了HashMap中的recordAccess方法(HashMap中該方法為空)闲孤,當(dāng)調(diào)用父類的put方法時(shí)谆级,在發(fā)現(xiàn)key已經(jīng)存在時(shí),會調(diào)用該方法讼积;當(dāng)調(diào)用自己的get方法時(shí)肥照,也會調(diào)用到該方法。

該方法提供了LRU算法的實(shí)現(xiàn)币砂,它將最近使用的Entry放到雙向循環(huán)鏈表的尾部建峭。也就是說,當(dāng)accessOrder為true時(shí)决摧,get方法和put方法都會調(diào)用recordAccess方法使得最近使用的Entry移到雙向鏈表的末尾亿蒸;當(dāng)accessOrder為默認(rèn)值false時(shí),從源碼中可以看出recordAccess方法什么也不會做掌桩。我們反過頭來边锁,再看一下addEntry方法:

/**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*
* LinkedHashMap中的addEntry方法
*/
void addEntry(int hash, K key, V value, int bucketIndex) {

    //創(chuàng)建新的Entry,并插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重寫了HashMap中的createEntry方法

    //雙向鏈表的第一個(gè)有效節(jié)點(diǎn)(header后的那個(gè)節(jié)點(diǎn))為最近最少使用的節(jié)點(diǎn)波岛,這是用來支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //如果有必要茅坛,則刪除掉該近期最少使用的節(jié)點(diǎn),  
    //這要看對removeEldestEntry的覆寫,由于默認(rèn)為false则拷,因此默認(rèn)是不做任何處理的贡蓖。  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        //擴(kuò)容到原來的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
} 

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry,這點(diǎn)與HashMap中相同 
    //創(chuàng)建新的Entry并將其鏈入到數(shù)組對應(yīng)桶的鏈表的頭結(jié)點(diǎn)處煌茬, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同時(shí)斥铺,都會將其插入到雙向鏈表的尾部,  
    //這樣就按照Entry插入LinkedHashMap的先后順序來迭代元素(LinkedHashMap根據(jù)雙向鏈表重寫了迭代器)
    //同時(shí)坛善,新put進(jìn)來的Entry是最近訪問的Entry晾蜘,把其放在鏈表末尾 邻眷,也符合LRU算法的實(shí)現(xiàn)  
    e.addBefore(header);  
    size++;  
}

同樣是將新的Entry鏈入到table中對應(yīng)桶中的單鏈表中,但可以在createEntry方法中看出剔交,同時(shí)也會把新put進(jìn)來的Entry插入到了雙向鏈表的尾部肆饶。
  
從插入順序的層面來說,新的Entry插入到雙向鏈表的尾部可以實(shí)現(xiàn)按照插入的先后順序來迭代Entry岖常,而從訪問順序的層面來說驯镊,新put進(jìn)來的Entry又是最近訪問的Entry,也應(yīng)該將其放在雙向鏈表的尾部腥椒。在上面的addEntry方法中還調(diào)用了removeEldestEntry方法阿宅,該方法源碼如下:

/**
 * Returns <tt>true</tt> if this map should remove its eldest entry.
 * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
 * inserting a new entry into the map.  It provides the implementor
 * with the opportunity to remove the eldest entry each time a new one
 * is added.  This is useful if the map represents a cache: it allows
 * the map to reduce memory consumption by deleting stale entries.
 *
 * <p>Sample use: this override will allow the map to grow up to 100
 * entries and then delete the eldest entry each time a new entry is
 * added, maintaining a steady state of 100 entries.
 * <pre>
 *     private static final int MAX_ENTRIES = 100;
 *
 *     protected boolean removeEldestEntry(Map.Entry eldest) {
 *        return size() > MAX_ENTRIES;
 *     }
 * </pre>
 *
 * <p>This method typically does not modify the map in any way,
 * instead allowing the map to modify itself as directed by its
 * return value.  It <i>is</i> permitted for this method to modify
 * the map directly, but if it does so, it <i>must</i> return
 * <tt>false</tt> (indicating that the map should not attempt any
 * further modification).  The effects of returning <tt>true</tt>
 * after modifying the map from within this method are unspecified.
 *
 * <p>This implementation merely returns <tt>false</tt> (so that this
 * map acts like a normal map - the eldest element is never removed).
 *
 * @param    eldest The least recently inserted entry in the map, or if
 *           this is an access-ordered map, the least recently accessed
 *           entry.  This is the entry that will be removed it this
 *           method returns <tt>true</tt>.  If the map was empty prior
 *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
 *           in this invocation, this will be the entry that was just
 *           inserted; in other words, if the map contains a single
 *           entry, the eldest entry is also the newest.
 * @return   <tt>true</tt> if the eldest entry should be removed
 *           from the map; <tt>false</tt> if it should be retained.
 */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

}

該方法是用來被重寫的,一般地笼蛛,如果用LinkedHashmap實(shí)現(xiàn)LRU算法洒放,就要重寫該方法。比如可以將該方法覆寫為如果設(shè)定的內(nèi)存已滿滨砍,則返回true往湿,這樣當(dāng)再次向LinkedHashMap中putEntry時(shí),在調(diào)用的addEntry方法中便會將近期最少使用的節(jié)點(diǎn)刪除掉(header后的那個(gè)節(jié)點(diǎn))惋戏。在第七節(jié)领追,筆者便重寫了該方法并實(shí)現(xiàn)了一個(gè)名副其實(shí)的LRU結(jié)構(gòu)。

get操作與標(biāo)志位accessOrder

public V get(Object key) {
    // 根據(jù)key獲取對應(yīng)的Entry响逢,若沒有這樣的Entry绒窑,則返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在這樣的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;
}

在LinkedHashMap中進(jìn)行讀取操作時(shí)舔亭,一樣也會調(diào)用recordAccess方法些膨。上面筆者已經(jīng)表述的很清楚了,此不贅述钦铺。

LinkedListMap與LRU小結(jié)

使用LinkedHashMap實(shí)現(xiàn)LRU的必要前提是將accessOrder標(biāo)志位設(shè)為true以便開啟按訪問順序排序的模式订雾。我們可以看到,無論是put方法還是get方法矛洞,都會導(dǎo)致目標(biāo)Entry成為最近訪問的Entry洼哎,因此就把該Entry加入到了雙向鏈表的末尾:get方法通過調(diào)用recordAccess方法來實(shí)現(xiàn);

put方法在覆蓋已有key的情況下沼本,也是通過調(diào)用recordAccess方法來實(shí)現(xiàn)噩峦,在插入新的Entry時(shí),則是通過createEntry中的addBefore方法來實(shí)現(xiàn)抽兆。這樣壕探,我們便把最近使用的Entry放入到了雙向鏈表的后面。多次操作后郊丛,雙向鏈表前面的Entry便是最近沒有使用的李请,這樣當(dāng)節(jié)點(diǎn)個(gè)數(shù)滿的時(shí)候,刪除最前面的Entry(head后面的那個(gè)Entry)即可厉熟,因?yàn)樗褪亲罱钌偈褂玫腅ntry导盅。

使用LinkedHashMap實(shí)現(xiàn)LRU算法

如下所示,筆者使用LinkedHashMap實(shí)現(xiàn)一個(gè)符合LRU算法的數(shù)據(jù)結(jié)構(gòu)揍瑟,該結(jié)構(gòu)最多可以緩存6個(gè)元素白翻,但元素多余六個(gè)時(shí),會自動(dòng)刪除最近最久沒有被使用的元素绢片,如下所示:

public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{

    private static final long serialVersionUID = 1L;

    public LRU(int initialCapacity,
             float loadFactor,
                        boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    /** 
     * @description 重寫LinkedHashMap中的removeEldestEntry方法滤馍,當(dāng)LRU中元素多余6個(gè)時(shí),
     *              刪除最不經(jīng)常使用的元素
     * @author rico       
     * @created 2017年5月12日 上午11:32:51      
     * @param eldest
     * @return     
     * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)     
     */  
    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
        // TODO Auto-generated method stub
        if(size() > 6){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {

        LRU<Character, Integer> lru = new LRU<Character, Integer>(
                16, 0.75f, true);

        String s = "abcdefghijkl";
        for (int i = 0; i < s.length(); i++) {
            lru.put(s.charAt(i), i);
        }
        System.out.println("LRU中key為h的Entry的值為: " + lru.get('h'));
        System.out.println("LRU的大小 :" + lru.size());
        System.out.println("LRU :" + lru);
    }
}

下圖是程序的運(yùn)行結(jié)果:


LinkedHashMap 有序性原理分析

如前文所述底循,LinkedHashMap 增加了雙向鏈表頭結(jié)點(diǎn)header 和 標(biāo)志位accessOrder兩個(gè)屬性用于保證迭代順序巢株。但是要想真正實(shí)現(xiàn)其有序性,還差臨門一腳熙涤,那就是重寫HashMap 的迭代器阁苞,其源碼實(shí)現(xiàn)如下:

private abstract class LinkedHashIterator<T> implements Iterator<T> {
    Entry<K,V> nextEntry    = header.after;
    Entry<K,V> lastReturned = null;

    /**
     * The modCount value that the iterator believes that the backing
     * List should have.  If this expectation is violated, the iterator
     * has detected concurrent modification.
     */
    int expectedModCount = modCount;

    public boolean hasNext() {         // 根據(jù)雙向列表判斷 
            return nextEntry != header;
    }

    public void remove() {
        if (lastReturned == null)
        throw new IllegalStateException();
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
    }

    Entry<K,V> nextEntry() {        // 迭代輸出雙向鏈表各節(jié)點(diǎn)
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
    }
}

// Key 迭代器,KeySet
private class KeyIterator extends LinkedHashIterator<K> {   
    public K next() { return nextEntry().getKey(); }
}

   // Value 迭代器祠挫,Values(Collection)
private class ValueIterator extends LinkedHashIterator<V> {
    public V next() { return nextEntry().value; }
}

// Entry 迭代器那槽,EntrySet
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() { return nextEntry(); }
}

從上述代碼中我們可以知道,LinkedHashMap重寫了HashMap 的迭代器等舔,它使用其維護(hù)的雙向鏈表進(jìn)行迭代輸出骚灸。

JDK1.8的改動(dòng)

原文是基于JDK1.6的實(shí)現(xiàn),實(shí)際上JDK1.8對其進(jìn)行了改動(dòng)慌植。
首先它刪除了addentry甚牲,createenrty等方法(事實(shí)上是hashmap的改動(dòng)影響了它而已)。

linkedhashmap同樣使用了大部分hashmap的增刪改查方法涤浇。
新版本linkedhashmap主要是通過對hashmap內(nèi)置幾個(gè)方法重寫來實(shí)現(xiàn)lru的鳖藕。

hashmap不提供實(shí)現(xiàn):

 void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

linkedhashmap的實(shí)現(xiàn):

處理元素被訪問后的情況

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;
        }
    }

處理元素插入后的情況

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }

處理元素被刪除后的情況

     void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<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;
}
}

另外1.8的hashmap在鏈表長度超過8時(shí)自動(dòng)轉(zhuǎn)為紅黑樹,會按順序插入鏈表中的元素只锭,可以自定義比較器來定義節(jié)點(diǎn)的插入順序著恩。

1.8的linkedhashmap同樣會使用這一特性,當(dāng)變?yōu)榧t黑樹以后蜻展,節(jié)點(diǎn)的先后順序同樣是插入紅黑樹的順序喉誊,其雙向鏈表的性質(zhì)沒有改表,只是原來hashmap的鏈表變成了紅黑樹而已纵顾,在此不要混淆伍茄。

總結(jié)

本文從linkedhashmap的數(shù)據(jù)結(jié)構(gòu),以及源碼分析施逾,到最后的LRU緩存實(shí)現(xiàn)敷矫,比較深入地剖析了linkedhashmap的底層原理例获。
總結(jié)以下幾點(diǎn):

1 linkedhashmap在hashmap的數(shù)組加鏈表結(jié)構(gòu)的基礎(chǔ)上,將所有節(jié)點(diǎn)連成了一個(gè)雙向鏈表曹仗。

2 當(dāng)主動(dòng)傳入的accessOrder參數(shù)為false時(shí), 使用put方法時(shí)榨汤,新加入元素不會被加入雙向鏈表,get方法使用時(shí)也不會把元素放到雙向鏈表尾部怎茫。

3 當(dāng)主動(dòng)傳入的accessOrder參數(shù)為true時(shí)收壕,使用put方法新加入的元素,如果遇到了哈希沖突轨蛤,并且對key值相同的元素進(jìn)行了替換蜜宪,就會被放在雙向鏈表的尾部,當(dāng)元素超過上限且removeEldestEntry方法返回true時(shí)祥山,直接刪除最早元素以便新元素插入圃验。如果沒有沖突直接放入,同樣加入到鏈表尾部枪蘑。使用get方法時(shí)會把get到的元素放入雙向鏈表尾部损谦。

4 linkedhashmap的擴(kuò)容比hashmap來的方便,因?yàn)閔ashmap需要將原來的每個(gè)鏈表的元素分別在新數(shù)組進(jìn)行反向插入鏈化岳颇,而linkedhashmap的元素都連在一個(gè)鏈表上照捡,可以直接迭代然后插入。

5 linkedhashmap的removeEldestEntry方法默認(rèn)返回false话侧,要實(shí)現(xiàn)lru很重要的一點(diǎn)就是集合滿時(shí)要將最久未訪問的元素刪除栗精,在linkedhashmap中這個(gè)元素就是頭指針指向的元素。實(shí)現(xiàn)LRU可以直接實(shí)現(xiàn)繼承l(wèi)inkedhashmap并重寫removeEldestEntry方法來設(shè)置緩存大小瞻鹏。jdk中實(shí)現(xiàn)了LRUCache也可以直接使用悲立。

參考文章
http://cmsblogs.com/?p=176

http://www.reibang.com/p/8f4f58b4b8ab

https://blog.csdn.net/wang_8101/article/details/83067860

https://www.cnblogs.com/create-and-orange/p/11237072.html

https://www.cnblogs.com/ganchuanpu/p/8908093.html

微信公眾號

Java技術(shù)江湖

如果大家想要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨的話,可以關(guān)注我的公眾號【Java技術(shù)江湖】一位阿里 Java 工程師的技術(shù)小站新博,作者黃小斜薪夕,專注 Java 相關(guān)技術(shù):SSM、SpringBoot赫悄、MySQL原献、分布式、中間件埂淮、集群姑隅、Linux、網(wǎng)絡(luò)倔撞、多線程讲仰,偶爾講點(diǎn)Docker、ELK痪蝇,同時(shí)也分享技術(shù)干貨和學(xué)習(xí)經(jīng)驗(yàn)鄙陡,致力于Java全棧開發(fā)冕房!

Java工程師必備學(xué)習(xí)資源: 一些Java工程師常用學(xué)習(xí)資源,關(guān)注公眾號后趁矾,后臺回復(fù)關(guān)鍵字 “Java” 即可免費(fèi)無套路獲取毒费。

我的公眾號

個(gè)人公眾號:黃小斜

黃小斜是跨考軟件工程的 985 碩士,自學(xué) Java 兩年愈魏,拿到了 BAT 等近十家大廠 offer,從技術(shù)小白成長為阿里工程師想际。

作者專注于 JAVA 后端技術(shù)棧培漏,熱衷于分享程序員干貨、學(xué)習(xí)經(jīng)驗(yàn)胡本、求職心得和程序人生牌柄,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+侧甫,全網(wǎng)已有10W+讀者珊佣。

黃小斜是一個(gè)斜杠青年,堅(jiān)持學(xué)習(xí)和寫作披粟,相信終身學(xué)習(xí)的力量咒锻,希望和更多的程序員交朋友,一起進(jìn)步和成長守屉!關(guān)注公眾號【黃小斜】后回復(fù)【原創(chuàng)電子書】即可領(lǐng)取我原創(chuàng)的電子書《菜鳥程序員修煉手冊:從技術(shù)小白到阿里巴巴Java工程師》

程序員3T技術(shù)學(xué)習(xí)資源: 一些程序員學(xué)習(xí)技術(shù)的資源大禮包惑艇,關(guān)注公眾號后,后臺回復(fù)關(guān)鍵字 “資料” 即可免費(fèi)無套路獲取拇泛。

本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布滨巴!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市俺叭,隨后出現(xiàn)的幾起案子恭取,更是在濱河造成了極大的恐慌,老刑警劉巖熄守,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜈垮,死亡現(xiàn)場離奇詭異,居然都是意外死亡柠横,警方通過查閱死者的電腦和手機(jī)窃款,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牍氛,“玉大人晨继,你說我怎么就攤上這事“峥。” “怎么了紊扬?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵蜒茄,是天一觀的道長。 經(jīng)常有香客問我餐屎,道長檀葛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任腹缩,我火速辦了婚禮屿聋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘藏鹊。我一直安慰自己润讥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布盘寡。 她就那樣靜靜地躺著楚殿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪竿痰。 梳的紋絲不亂的頭發(fā)上脆粥,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音影涉,去河邊找鬼变隔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛常潮,可吹牛的內(nèi)容都是我干的弟胀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼喊式,長吁一口氣:“原來是場噩夢啊……” “哼孵户!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起岔留,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤夏哭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后献联,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竖配,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年里逆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了进胯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡原押,死狀恐怖胁镐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤盯漂,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布颇玷,位于F島的核電站,受9級特大地震影響就缆,放射性物質(zhì)發(fā)生泄漏帖渠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一竭宰、第九天 我趴在偏房一處隱蔽的房頂上張望空郊。 院中可真熱鬧,春花似錦切揭、人聲如沸渣淳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鄙漏,卻和暖如春嗤谚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怔蚌。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工巩步, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留甥雕,地道東北人宪赶。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓症脂,卻偏偏與公主長得像熏兄,于是被迫代替她去往敵國和親湿弦。 傳聞我的和親對象是個(gè)殘疾皇子酥艳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容