網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
上一篇講了Collection、Queue和Deque、List或Set,沒(méi)看的朋友可以去簡(jiǎn)單看看,這一篇主要講和Map相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
Map
上篇有介紹過(guò),Map是另一種數(shù)據(jù)結(jié)構(gòu),它獨(dú)立于Collection,它的是一個(gè)接口,它的抽象實(shí)現(xiàn)是AbstractMap,它內(nèi)部是會(huì)通過(guò)Set來(lái)實(shí)現(xiàn)迭代器
Set<Map.Entry<K, V>> entrySet();
是和Set有關(guān)聯(lián)的,思想上主要以key-value的形式存儲(chǔ)數(shù)據(jù),但是具體的實(shí)現(xiàn)會(huì)交給子類去實(shí)現(xiàn)。
ArrayMap
ArrayMap和ArraySet一樣,內(nèi)部用兩個(gè)數(shù)組實(shí)現(xiàn)int[] mHashes好Object[] mArray
不同于ArraySet的是,ArraySet的mHashes是用Object的hash,而ArrayMap的mHashes是用key的hash。
TreeMap
上一篇講的TreeSet內(nèi)部也是用的TreeMap。TreeMap是一個(gè)紅黑樹(shù)的數(shù)據(jù)結(jié)構(gòu),如果不了解紅黑樹(shù)的話,直接看會(huì)比較懵逼,不過(guò)沒(méi)關(guān)系,我們可以往上一級(jí)去看,紅黑樹(shù)其實(shí)是一種二叉搜索樹(shù),那什么是二叉搜索樹(shù)呢?簡(jiǎn)單來(lái)說(shuō),二叉搜索樹(shù)是任何一個(gè)節(jié)點(diǎn)的左子樹(shù)都小于當(dāng)前節(jié)點(diǎn),任何一個(gè)節(jié)點(diǎn)的右子樹(shù)都大于當(dāng)前節(jié)點(diǎn)。
這樣講就更容易能理解了吧。那這棵樹(shù)的順序就是中序遍歷的順序,它也符合二分查找的條件。如果還是不理解的話可以先去了解一下二叉搜索樹(shù),這比紅黑樹(shù)更容易理解。二叉搜索樹(shù)在插入節(jié)點(diǎn)之后,要實(shí)現(xiàn)平衡,通常可以通過(guò)左旋和右旋去實(shí)現(xiàn)(這個(gè)算法這里也不詳細(xì)說(shuō),感興趣的可以自己去了解,不感興趣的可以先記住這個(gè)概念)。而紅黑樹(shù)要達(dá)到平衡,通過(guò)左旋和右旋之外還有可能需要實(shí)現(xiàn)變色,這個(gè)相對(duì)于二叉搜索樹(shù)來(lái)說(shuō)會(huì)相對(duì)更加復(fù)雜。但是沒(méi)關(guān)系,先記住這個(gè)概念。它的特點(diǎn)主要是查找通過(guò)二分查找,插入會(huì)通過(guò)左旋右旋和變色來(lái)維持平衡。
這里也可以擴(kuò)展一下紅黑樹(shù)的特征,但是不會(huì)詳細(xì)的介紹
1. 結(jié)點(diǎn)是紅色或黑色
2. 根結(jié)點(diǎn)是黑色
3. 所有葉子都是黑色
4. 每個(gè)紅色結(jié)點(diǎn)的兩個(gè)子結(jié)點(diǎn)都是黑色
5. 從任一結(jié)點(diǎn)到其每個(gè)葉子的所有路徑都包含相同數(shù)目的黑色結(jié)點(diǎn)
其內(nèi)部的TreeMapEntry是它的結(jié)構(gòu)體
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
K key;
V value;
TreeMapEntry<K,V> left;
TreeMapEntry<K,V> right;
TreeMapEntry<K,V> parent;
boolean color = BLACK;
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
TreeMapEntry(K key, V value, TreeMapEntry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
/**
* Returns the key.
*
* @return the key
*/
public K getKey() {
return key;
}
/**
* Returns the value associated with the key.
*
* @return the value associated with the key
*/
public V getValue() {
return value;
}
/**
* Replaces the value currently associated with the key with the given
* value.
*
* @return the value associated with the key before this method was
* called
*/
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
能看到除了key-value之外,還有l(wèi)eft,right,parent表示左子節(jié)點(diǎn),右子節(jié)點(diǎn)和父節(jié)點(diǎn),還有一個(gè)boolean類型的color表示這個(gè)節(jié)點(diǎn)是紅色的還是黑色的。也可以簡(jiǎn)單看看它的put方法
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
......
int cmp;
TreeMapEntry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
......
}
Comparator能實(shí)現(xiàn)自己的排序規(guī)則,一般都是默認(rèn)的規(guī)則。通過(guò)compare去找到合適的位置插入紅黑樹(shù),這段代碼還是沒(méi)什么難的地方,也不詳細(xì)去講了。
HashMap
這個(gè)相對(duì)而言就比較重要,因?yàn)橛玫谋容^多,所以可能會(huì)講的相對(duì)更詳細(xì)。可以先看看它的內(nèi)部的數(shù)據(jù)結(jié)構(gòu),內(nèi)部是一個(gè)節(jié)點(diǎn)數(shù)組Node<K,V>[] table,每個(gè)節(jié)點(diǎn)又是一個(gè)鏈表(或紅黑樹(shù))的結(jié)構(gòu)。
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; }
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;
}
}
節(jié)點(diǎn)主要有3個(gè)重要的參數(shù),key,value和key的hash。
那我們就先需要搞懂它的一個(gè)邏輯,它會(huì)想用Key去生成hash,再用hash去計(jì)算出這個(gè)節(jié)點(diǎn)在數(shù)組中的下標(biāo)。所以先看計(jì)算key的hash的方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
拿到key的hashcode,然后讓這個(gè)hashcode的高16位和低16位做異或運(yùn)算。這個(gè)操作是為了什么呢?這個(gè)是為了降低哈希沖突的概率,那這里又引出了什么是hash沖突?
這里直接說(shuō)會(huì)比較難去理解,沒(méi)關(guān)系,我們先往下講如何通過(guò)hash去計(jì)算數(shù)組的位置,理解完這個(gè)步驟之后我們?cè)俜椿貋?lái)講哈希沖突這個(gè)事。
在HashMap的put方法中有一段代碼
......
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
......
這個(gè)(n - 1) & hash就是計(jì)算數(shù)組下標(biāo)的方式,通過(guò)hash和數(shù)組長(zhǎng)度-1做與運(yùn)算,來(lái)得到這個(gè)節(jié)點(diǎn)應(yīng)該插入到數(shù)組的哪個(gè)位置。那為何要用hash和數(shù)組長(zhǎng)度-1做與運(yùn)算,這個(gè)數(shù)組長(zhǎng)度-1是什么東西?這又要涉及到另一個(gè)知識(shí)點(diǎn),所以說(shuō)hashmap中的細(xì)節(jié)比較多。
首先這個(gè)hashmap的長(zhǎng)度,必須是2的n次方,比如4,8,16,32。 它內(nèi)次擴(kuò)容也是x2,這時(shí)候我們?cè)偃タ撮L(zhǎng)度-1,比如長(zhǎng)度是16,那16-1是15,它對(duì)應(yīng)的二進(jìn)制是1111,假如長(zhǎng)度是32,那31的二進(jìn)制是11111。看到這里相信你已經(jīng)知道長(zhǎng)度-1代表什么了。講到長(zhǎng)度,這里又會(huì)涉及一個(gè)知識(shí)點(diǎn)是為什么HashMap的默認(rèn)長(zhǎng)度是16,定8不行嗎?定32不行嗎?這個(gè)放到最后講。
我們回過(guò)來(lái)先繼續(xù)去說(shuō)哈希沖突這事,什么是哈希沖突?hash沖突的意思是兩個(gè)不同的對(duì)象,計(jì)算出來(lái)的哈希值相同。放在HashMap中你也可以簡(jiǎn)單理解成,兩個(gè)不同的key計(jì)算出的數(shù)組下標(biāo)相同。而HashMap中解決哈希沖突的方法是上面的高16位和低16位做異或,和用鏈地址法(就是相同的話,節(jié)點(diǎn)用鏈表或者紅黑樹(shù)表示)
鏈地址法比較容易理解,主要還是為什么hash的高16位和低16位做異或能降低哈希沖突的概率。那是因?yàn)槠匠S?jì)算的時(shí)候,高16位是不會(huì)參與計(jì)算的, 我舉個(gè)例子,假如兩個(gè)不同的key計(jì)算出來(lái)的hash值,高16位不同,低16位相同,數(shù)組長(zhǎng)度是16,這時(shí)候你這兩個(gè)hash與15做與運(yùn)算,得到的結(jié)果相同吧,那不就沖突了?如果我把高16位和低16位先做異或,這時(shí)候會(huì)讓高16位參與到運(yùn)算中,所以他們計(jì)算出的結(jié)果就會(huì)不同。
此時(shí)可能有人會(huì)想,那為什么不把32位的hash分4段做異或,4段8位做異或,這樣不就更充分嗎?這樣確實(shí)能更先降低數(shù)組長(zhǎng)度是16情況下的哈希沖突概率,但是你要考慮整形的最大值,當(dāng)數(shù)組的長(zhǎng)度-1達(dá)到16位時(shí),這樣做就會(huì)出現(xiàn)問(wèn)題。
通過(guò)Key計(jì)算出數(shù)組的位置,如果這個(gè)位置沒(méi)節(jié)點(diǎn),就把這個(gè)節(jié)點(diǎn)放入,如果有節(jié)點(diǎn),就遍歷這個(gè)節(jié)點(diǎn)的鏈表,所以我們看put方法具體的操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 判斷數(shù)組中沒(méi)結(jié)點(diǎn)的話創(chuàng)建結(jié)點(diǎn)然后添加進(jìn)取
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判斷結(jié)點(diǎn)的hash和key是相等的話直接改值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 判斷是紅黑樹(shù)的情況(后面會(huì)說(shuō))
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 判斷是鏈表的情況,循環(huán)遍歷鏈表找到hash和key相同的結(jié)點(diǎn)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 沒(méi)找到的情況下創(chuàng)建一個(gè)新的結(jié)點(diǎn)放到鏈表末尾
p.next = newNode(hash, key, value, null);
// 判斷是否需要將鏈表轉(zhuǎn)成紅黑樹(shù)
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;
}
}
// 找到相同的key,直接替換value然后結(jié)束流程
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 擴(kuò)容
if (++size > threshold)
resize();
// 鉤子
afterNodeInsertion(evict);
return null;
}
從中能看出put內(nèi)部的主要操作是判斷對(duì)應(yīng)數(shù)組的位置是否有結(jié)點(diǎn),沒(méi)有的話直接放進(jìn)去,有的話遍歷這個(gè)結(jié)點(diǎn)的鏈表或者紅黑樹(shù),找到hash和key相同的結(jié)點(diǎn)替換掉,如果沒(méi)有,則新建結(jié)點(diǎn)放到尾部,如果鏈表的長(zhǎng)度到達(dá)8,將鏈表轉(zhuǎn)成紅黑樹(shù)。最后再判斷是否要擴(kuò)容。
這段代碼還是比較容易理解的,先講講最后的afterNodeInsertion,鉤子函數(shù),雖然在這里面沒(méi)有任何代碼,但從設(shè)計(jì)的角度去看,這是一個(gè)比較好的設(shè)計(jì),可以增強(qiáng)擴(kuò)展性。
比較重要的操作就是轉(zhuǎn)紅黑樹(shù)的操作,可以看看它的常量定義
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
可以看到這里小于6個(gè)的時(shí)候轉(zhuǎn)回鏈表,轉(zhuǎn)的操作在resize的split方法。說(shuō)到resize,我們也可以看看,這個(gè)方法也算重點(diǎn)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
......
int newCap, newThr = 0;
if (oldCap > 0) {
......
// 重點(diǎn)是newCap = oldCap << 1
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
......
}
......
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)
// 單結(jié)點(diǎn)情況下的擴(kuò)容
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 紅黑樹(shù)情況下的擴(kuò)容
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 鏈表情況下的擴(kuò)容
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;
}
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;
}
我這里屏蔽了一些代碼,只保留重點(diǎn)的代碼,能簡(jiǎn)單的看出擴(kuò)容的操作就是創(chuàng)建另一個(gè)數(shù)組,然后給新數(shù)組賦值后覆蓋舊數(shù)組。如果看過(guò)上一篇文章的ArrayList,那就能很容易知道,短時(shí)間頻繁的擴(kuò)容會(huì)導(dǎo)致內(nèi)存抖動(dòng),所以為什么初始長(zhǎng)度不定2,不定4,不定8 。然后看最主要的擴(kuò)容操作newCap = oldCap << 1,新的長(zhǎng)度是舊的長(zhǎng)度左移動(dòng)一位,其實(shí)就是x2,所以上面有說(shuō)hashmap的長(zhǎng)度都是2的n次方。
然后看看3種不同情況的擴(kuò)容,先看單結(jié)點(diǎn)的情況,如果數(shù)組中對(duì)應(yīng)的位置只有一個(gè)結(jié)點(diǎn),那就重新計(jì)算它的下標(biāo),然后換個(gè)位置。
再先看如果是鏈表的情況。它會(huì)把鏈表拆分成兩條鏈表,分別放到數(shù)組的newTab[j]和newTab[j + oldCap] ,這個(gè)大概的思路是這樣,詳細(xì)的話多看幾次代碼也能很容易理解。最后再來(lái)看紅黑樹(shù)的情況。調(diào)用split方法
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
這里只需要簡(jiǎn)單理解,TreeNode這個(gè)數(shù)據(jù)結(jié)構(gòu)是繼承Node的,for循環(huán)中的操作其實(shí)就和鏈表中的操作一樣,分成low和height兩部分,然后判斷小于UNTREEIFY_THRESHOLD也就是6的情況下,轉(zhuǎn)成Node結(jié)點(diǎn),也就是樹(shù)轉(zhuǎn)回鏈表。
總結(jié)
這次主要講了ArrayMap、TreeMap和HashMap3個(gè)數(shù)據(jù)結(jié)構(gòu),因?yàn)檫@3個(gè)用得比較多,但并不是Map中只有這3個(gè),比如Map中還有IdentityHashMap、WeakHashMap這些,但是比較常用的就是這3種,其中最重要的是HashMap。
HashMap中比較重要的是它的一些設(shè)計(jì)的思想,比如如何去解決和降低哈希沖突,為什么默認(rèn)的大小是16,其次要了解它的一個(gè)工作原理,我這里主要是以put方法來(lái)舉例,當(dāng)中就涉及到像鏈地址法,鏈表轉(zhuǎn)紅黑樹(shù),擴(kuò)容等操作。和LinkedList一樣,我也十分建議沒(méi)看過(guò)HashMap源碼的人能去看看,體驗(yàn)一下它內(nèi)部的一些代碼設(shè)計(jì)思想和細(xì)節(jié)的處理。
通過(guò)這兩篇文章,平常比較常用的數(shù)據(jù)結(jié)構(gòu)基本都講完了,下一篇就開(kāi)始講講和線程安全相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
原文鏈接:https://juejin.cn/post/7173646128360275999
相關(guān)推薦
- 2022-11-29 ASP.NET?MVC把數(shù)據(jù)庫(kù)中枚舉項(xiàng)的數(shù)字轉(zhuǎn)換成文字_基礎(chǔ)應(yīng)用
- 2022-12-29 R語(yǔ)言apply系列函數(shù)實(shí)例詳解_R語(yǔ)言
- 2022-05-13 Android水波紋效果
- 2022-11-23 Python面向?qū)ο蟮膬?nèi)置方法梳理講解_python
- 2022-04-20 基于PyQT5制作一個(gè)桌面摸魚(yú)工具_(dá)python
- 2022-06-01 python?嵌套型partials的使用_python
- 2023-07-08 SparkMD5獲取不同圖片的md5顯示相同,解決辦法
- 2022-12-25 一文帶你熟悉Go語(yǔ)言中的for循環(huán)_Golang
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支