hashmap
是我們最常用的集合型別之一了。也由於其高效、使用方便,我們有必要對其內部進行梳理一番。
jdk1.8原始碼中,關於map
的類圖關係如下:
從map
的類圖關係中,我們可以看出還是蠻豐富的。需要用到順序的,可以使用treemap
,需要執行緒安全的可以使用hashtable
和concurrenthashmap
。其各自特點總結如下:類特點
hashmap
儲存資料無序;非執行緒安全;key可為,但是其桶索引為0;效率為o(1)
hashtable
遠古時代就具有的,儲存資料無序;執行緒安全,但是是使用笨重的synchronized
;key不可為
concurrenthashmap
新貴,儲存資料無序;執行緒安全,採用cas
;key不可為
treemap
儲存資料有序;非執行緒安全
linkedhashmap
儲存資料有序;非執行緒安全
讓我們先從hashmap
開始。
jdk1.7中,hashmap
由陣列和鍊錶構成,當鍊表資料特別多的時候,很明顯的其效率受到影響。於是,在jdk1.8中的hashmap
當鍊表資料過長時,轉為紅黑樹的資料結構。
我們選取常用的幾個方法(例項化、put)來看下原始碼。
//map預設初始化的容量
static final int default_initial_capacity = 1 << 4; // aka 16
//map的最大容量
static final int maximum_capacity = 1 << 30;
//預設負載因子
static final float default_load_factor = 0.75f;
//鍊錶轉為紅黑樹的閾值
static final int treeify_threshold = 8;
//紅黑樹轉為鍊錶的閾值
static final int untreeify_threshold = 6;
//鍊錶轉為紅黑樹表的閾值
static final int min_treeify_capacity = 64;
//桶陣列
transient node table;
//對map的entryset結果快取
transient set> entryset;
//key-value對的數量
transient int size;
//增加或者刪除map元素、rehash的次數
transient int modcount;
//hashmap需要resize的閾值
int threshold;
//負載因子
final float loadfactor;
選取個複雜點的構造方法:
//initialcapacity表示hashmap的容量,loadfactor表示負載因子
public hashmap(int initialcapacity, float loadfactor)
//tablesizefor的方法如下
//目前演算法我看的不太明白
static final int tablesizefor(int cap)
大家通過源**可以看到,初始化時,可以說幾乎沒發生沒什麼事情。只賦值了loadfactor
和確保map
的容量為2的冪次方。
這裡就有個問題?為什麼需要確保map
的容量為2的冪次方?其實這是個非常規的設計,常規的設計是把桶的大小設計為素數(參考:在講完put
方法後我們來闡述。
其實,這裡可以看作是map
的延遲初始化。在首次put
元素時,會初始化屬性table
。在這裡順便提下table
的型別node
。在為鍊錶時,其資料結構為:
//鍊錶節點
static class nodeimplements map.entry
當轉為紅黑樹時,其結構為:
//樹節點
static final class treenodeextends linkedhashmap.entry
final v putval(int hash, k key, v value, boolean onlyifabsent,
boolean evict)
下乙個節點不為空,判斷key是否相同,相同則覆蓋舊值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;}}
v oldvalue = e.value;
if (!onlyifabsent || oldvalue == null)
e.value = value;
//為linkedhashmap預備
afternodeaccess(e);
return oldvalue;}}
//新增修改次數
++modcount;
//超過閾值則分配空間,重新rehash運算
if (++size > threshold)
resize();
//為linkedhashmap預備
afternodeinsertion(evict);
return null;
}
我們可以畫個流程圖:
其他的方法,譬如get
、remove
等,主要的都是先要確定key
對應的索引。即hash & (n - 1)
。其中hash
為key
對應的hash
值,n
為capacity
,即map
的容量。
我們先來看下hash
的計算:
static final int hash(object key)
為什麼hashcode
需要與其高16為抑或?簡單點說就是讓高位和低位都參與運算,使key
的分布盡量更均勻些。參見
有了hash
值之後,我們可以計算索引的位置了。一般都是取模運算,但是大家都知道計算機是二進位制的,位運算會比取余快多了。所以這裡的hash & (n - 1)
可以看做是用空間來換取時間。因為當n
為2的冪次方時,n-1
的二進位制恰恰全是1
,其與hashcode
的二進位制與值正好是取模的結果。這裡就回答了上面為什麼需要確保map
的容量為2的冪次方的問題。
hashmap
的原始碼目前就分析到這裡。流程其實說起來不是很難,難的關鍵就是為什麼設計者會是這樣的設計?這恰恰是我們需要多思考的。本篇在關於這一點上還是遠遠不足的,大家可以多搜尋幾篇來看看,多思考思考。
HashMap原始碼分析 JDK1 8
陣列 鍊錶 紅黑樹 陣列儲存元素,當有衝突時,就在該陣列位置形成乙個鍊錶,儲存衝突的元素,當鍊表元素個數大於閾值時,鍊錶就轉換為紅黑樹。transient node table transient int size int threshold final float loadfactor 預設初始容...
HashMap原始碼分析 (JDK1 8
首先,hashmap儲存結構類似於位桶,總體結構是 位桶 鍊錶 紅黑樹,這與之前版本的實現有所改進。常量域預設容量16 static final int default initial capacity 1 4 最大容量2的30次方 static final int maximum capacity...
HashMap原始碼分析jdk1 8
hashmap作為一種最常用的集合型別之一,他的實現是用的雜湊表,在這就不進行雜湊表詳細的解釋。為解決雜湊表的衝突問題,hashmap即是採用了鏈位址法,也就是陣列 鍊錶的方式。廢話不多說,我們還是通過原始碼來進行hashmap的詳解。組成hashmap的基本單元node節點 node節點的定義 s...