分散式Java應用之集合框架篇(下)
前言: 在分散式Java應用之集合框架篇(上)一文中,從整體上對Java分散式應用中的集合框架進行了介紹,以及對於其中的List家族給出了原始碼分析;本文將繼續介紹集合框架中的Set家族和Map家族,其實Set家族和Map家族之間是有著很深的淵源,在本文的後續內容中,將從兩大家族的成員的關鍵實現進行原始碼層面的分析!
首先,還是給出集合框架的整體類圖關係,通過類圖展開下面的介紹;
對於Collection介面的子介面Set來說,介面的實現類同樣是存放一系列有序的元素,且這些元素均為一個個單體物件,相比於List家族中的實現類而言,最大的區別在於List介面的實現類可以存放重複的元素(即元素之間通過==或equals判斷為true),而Set介面的實現類不可以存放重複的元素;
我們先看一下,Set家族中常用的實現類HashSet,下面是HashSet類的底層實現原始碼:
//底層元素的儲存結構
private transient HashMap<E,Object> map;
/**
* 構造一個新的空集合,實際上就是構造了一個初始容量為16,負載因子為0.75的HashMap物件作為底層儲存
*/
public HashSet() {
map = new HashMap<>();
}
/**
* 構造一個新的空集合,實際上就是構造了一個初始容量為指定容量,負載因子為指定負載因子的HashMap物件作為底層儲存
* @param initialCapacity 初始化容量
* @param loadFactor 負載因子
* @throws IllegalArgumentException 如果初始化容量引數為負數,或負載因子不為正數,那麼將會丟擲異常
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
/**
* 構造一個新的空集合,實際上就是構造了一個初始容量為指定容量,負載因子為預設負載因子0.75的HashMap物件作為底層儲存
* @param initialCapacity 初始化容量
* @throws IllegalArgumentException 如果初始化容量為負數,則丟擲異常
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
從上述原始碼可以看出,HashSet底層採用HashMap實現,因此想要理解HashSet的具體實現原理,首先需要搞清楚HashMap的底層實現原理,而HashMap又是Map家族的成員,這就正好印證了本文開頭處給出了言論:Set家族與Map家族是有很深的淵源的,底層都是相似的,淵源當然不會小!
那麼,我們就將Set家族放下,先看一看Map家族的核心成員及其實現原理;通過集合框架圖可以看出,其實研究Map也就是研究HashMap,下面對HashMap類的底層結構以及常用操作的實現進行原始碼分析;
HashMap底層儲存結構:
/**
* 採用陣列實現的底層儲存結構,陣列元素型別是內部類Node,陣列在有必要的時候,
* 會進行擴容;當為陣列分配記憶體時,陣列的大小始終保持為2的整數次冪(JDK1.8中的HashMap
* 相關優化的基礎)
*/
transient Node<K,V>[] table;
從這裡可以看出,HashMap底層的基本資料結構是陣列,而陣列的元素型別是Node<K,V>,下面是Node<K,V>的原始碼:
/**
* 基本的hash節點類,HashMap大多數元素都是以這種結構進行包裝存放的
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//雜湊值
final K key;//鍵
V value;//值
Node<K,V> next;//關鍵欄位,底層結構的實現基礎
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//final修飾的方法,不可以被重寫,保證hashcode求法的一致性
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//核心方法,用於比較節點是否相同
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
從原始碼可以看出Node<K,V>實現了Map介面中的子介面Entry<K,V>,關鍵看一下Node<K,V>的欄位,可以看到,每一個Node<K,V>物件都有四個欄位,分別是hash、key、value以及next;hash、key、value不用說,直接看next,next屬性是Node<K,V>型別的,這與連結串列是相同的,那麼可以斷定Node<K,V>是一個連結串列節點類;
小結: HashMap底層採用的是陣列+連結串列實現的儲存結構,每一個數組元素都可以看做是一個連結串列;
下面看一下HashMap物件的常用操作:
新增鍵值對:put(K key, V value)
/**
* 計算鍵的雜湊值,然後將雜湊值的高16位與低16位按位與,得到的結果作為最後參與雜湊桶定位的雜湊值
* 這樣可以保證當陣列長度較小時,高位能夠參與雜湊桶的定位操作,這是在速度、效率以及質量方面對定位
* 操作的一種折中處理
*/
static final int hash(Object key) {
int h;
//如果key為null,則預設在陣列的第一個桶中進行插入
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 初始化陣列或者擴容2倍,如果為null,則按照threshold的值分配陣列;
* 否則,擴容兩倍,優化向新陣列中移動元素時的操作
* @return 新的陣列
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//判斷當前陣列是否為null,如果為null,說明還沒有初始化陣列
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//如果當前陣列的容量大於0,說明需要進行擴容
if (oldCap > 0) {
//如果當前陣列的容量已經超過MAXIMUM_CAPACITY,那麼不可以進一步擴容,將threshold賦值為最大上限值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果當前陣列的容量沒有超過MAXIMUM_CAPACITY,那麼就擴大兩倍,同時將新陣列對應的threshold也擴大兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果當前陣列的容量不大於0,說明需要初始化,如果當前陣列的threshold大於0,那麼threshold的值就是新的陣列容量
else if (oldThr > 0)
// initial capacity was placed in threshold
newCap = oldThr;
else {
// 否則表示使用預設的初始化容量作為陣列的新容量
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果當前threshold為0,那麼就會計算新的threshold
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新threshold
threshold = newThr;
//建立新陣列,並將舊陣列中的元素移動到新陣列中
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 保持原有順序對連結串列中的元素進行遷移
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//原位置不動
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//移動oldCap個位置
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
// 構造一個常規節點(非樹節點)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
/**
* 將以key和value對映的鍵值對插入HashMap中,如果HashMap中已經存在以key為鍵的鍵值對
* 那麼就以value替換已有鍵值對的值
* @return 如果已存在以key為鍵的鍵值對,那麼就返回舊值;否則,返回null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 實現Map介面中的put方法
* @param hash 鍵的雜湊值
* @param key 鍵
* @param value 值
* @param onlyIfAbsent 如果為true,那麼不會修改已存在的鍵值對
* @param evict 如果為false,那麼陣列處於建立模式
* @return 返回舊值或null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//判斷當前table屬性是否為null或者table屬性的長度是否為0;如果是則呼叫resize()對table進行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//利用陣列長度減一的值與hash進行按位與,來定位鍵值對應該插入的桶位置
//判斷該位置處的陣列元素是否為null;如果為null,則構造一個新的Node物件放到陣列的該位置上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果不為null,發生雜湊衝突
Node<K,V> e; K k;
//如果陣列元素的雜湊值與待插入鍵值對的雜湊值相等且鍵相等,則找到衝突元素,更新該鍵值對的值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果陣列元素與待插入鍵值對沒有發生衝突,則判斷陣列元素的型別
else if (p instanceof TreeNode)
//陣列元素為TreeNode型別,呼叫TreeNode的putTreeVal方法將鍵值對進行插入處理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//陣列元素為Node型別,則按照連結串列結構進行查詢插入
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//如果到了連結串列尾部都沒有找到相等的鍵值對,就將鍵值對插入連結串列尾部
p.next = newNode(hash, key, value, null);
//如果當前位置的桶中的節點數超過TREEIFY_THRESHOLD,就將連結串列結構轉換為紅黑樹結構,結束查詢
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到相等的鍵值對,就結束迴圈
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不為null,說明查詢到相等的鍵值對,那麼替換已有的鍵值對中的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//結構調整次數加一
++modCount;
//如果插入後的鍵值對數量超過threshold,則進行擴容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
小結: 對於鍵值對的put操作,需要注意的地方有:雜湊值的求取、雜湊桶定位、連結串列與紅黑樹轉換、擴容以及擴容之後鍵值對的移動(保持原有連結串列的順序),key為null元素的插入位置、鍵值對在連結串列尾部插入等;
put操作的流程梳理: 首先,判斷當前陣列是否還沒有初始化,沒有初始化先初始化,如果初始化過了,那麼進行雜湊值的求取以及雜湊桶的定位;定位之後,根據陣列元素是否為null,進行分情況處理;如果元素為null,直接插入鍵值對,如果不為null,則判斷是否鍵相等,相等替換舊值,如果不相等,進入下一步;判斷陣列元素的型別,如果是紅黑樹,則按照紅黑樹的方法進行插入,如果是連結串列則按照連結串列的方式進行插入,插入的過程中需要判斷是否需要進行連結串列與紅黑樹之間的轉換;插入之後,判斷是否需要進行擴容,如果需要擴容,則進行擴容;否則,方法結束
類似的常用操作還有刪除鍵值對:remove(Object key),獲取鍵值對:get(Object key)等等,但是核心的東西在上面的方法中已經給出,不同的方法關鍵的東西差不多,所以這裡不再詳述;
通過分析我們對HashMap的關鍵實現已經有了一個大概的理解,下面回過頭來看一下文章開頭提到的HashSet,現在看HashSet就可以知道,底層採用的是HashMap來實現的,放入HashSet中的元素都是作為鍵值對的key放入底層的HashMap例項中的,而鍵值對中的value對於HashSet來說是無關緊要的,所以每一個鍵值對都會共用一個Object物件作為value;
既然是基於HashMap實現HashSet的,那麼HashSet中的常用操作也就利用HashMap中對應的方法來實現,因此這裡就不給出原始碼分析,相信理解了HashMap的操作就可以類推出HashSet的操作是如何實現的!
以上就是Set家族和Map家族中的最常用的實現類成員,通過上述的分析,對於這兩個家族有了比較深的認識和理解,其實對於HashMap其實還有一個比較關鍵的東西尚未提及,比如JDK1.8之前,多執行緒併發操作HashMap例項可能會出現死迴圈,導致CPU佔用率一致飆高等問題,但是由於本文只是對常用實現類的常用操作的實現進行分析,故此處不再贅述其他內容;
總結: 通過本文以及分散式Java應用之集合框架篇(上)這兩篇文章,我們對於集合框架中的常用實現類有了一定的認識,但是細細觀察可以發現,其實這些常用實現類中大都不是執行緒安全的,在多執行緒、高併發大行其道的今天,對於執行緒安全的問題十分關注,那麼,我們就必須掌握如何在併發的場景下使用這些集合類,後面將會對JDK中併發包JUC中的常用類進行分析,由於個人的理解能力有限,因此有不對的地方還希望讀者可以指出,不勝感激!