《Java集合詳解系列》是我在完成夯實(shí)Java基礎(chǔ)篇的系列博客后準(zhǔn)備開始寫的新系列。
這些文章將整理到我在GitHub上的《Java面試指南》倉庫吏奸,更多精彩內(nèi)容請到我的倉庫里查看
喜歡的話麻煩點(diǎn)下Star黎休、fork哈
文章首發(fā)于我的個(gè)人博客:
今天我們來深入探索一下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)是按插入順序排序的礁芦。
本質(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)的位置上之外烹骨,還會將其插入到雙向鏈表的尾部翻伺。
更直觀地,下圖很好地還原了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ā)布滨巴!