簡(jiǎn)書(shū) 加薪貓
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處枪向,謝謝!
這一系列主要介紹HashMap(1.8)彼硫,記錄也是分享吃溅,歡迎討論
0.HashMap 結(jié)構(gòu)
HashMap 的數(shù)據(jù)存在哪里溶诞?數(shù)據(jù)結(jié)構(gòu)是什么?
1.HashMap所有的key-value,存在一個(gè)全局變量Node<K,V>[] table里面决侈。
2.Node<K,V> 這個(gè)的結(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;
}
}
hash用來(lái)存儲(chǔ)這個(gè)Node中key經(jīng)過(guò)HashMap的hash(K key)方法計(jì)算后得出的螺垢,key、value不說(shuō)了赖歌,next存儲(chǔ)一下個(gè)Node節(jié)點(diǎn)枉圃。HashMap是數(shù)組+鏈表的形式存儲(chǔ)的,當(dāng)然這個(gè)Node的子類還有TreeNode庐冯,這個(gè)我們之后再說(shuō)
哈希值是否有重復(fù)孽亲?為什么要用鏈表存儲(chǔ)?
按理說(shuō)哈希值是不會(huì)有重復(fù)的展父,java Object類中的hashCode方法使用類的地址轉(zhuǎn)int返劲,保證了hash值的唯一性,雖說(shuō)哈希值不會(huì)重復(fù)栖茉,但是在存儲(chǔ)時(shí)我們還是會(huì)發(fā)生沖突的篮绿,具體我們可以看下面的介紹,用鏈表存儲(chǔ)就是為了解決沖突問(wèn)題的吕漂,具體可以仔細(xì)研究1.1.2節(jié)亲配。其次1.8不僅用鏈表,當(dāng)鏈表長(zhǎng)度超過(guò)默認(rèn)值(8)時(shí),HashMap還會(huì)把這個(gè)鏈表轉(zhuǎn)為紅黑樹(shù)吼虎,這也是為了提升查找效率犬钢。
正文
HashMap源碼中最有用,最值得看的就是resize()擴(kuò)容方法思灰,直接去看resize()方法肯定是一頭霧水玷犹。
所以這里是從我們最常用的方法一步步去分享。
1. get(Object key)
直接上代碼
public V get(Object key) {
Node e;
return(e = getNode(hash(key),key)) ==null?null: e.value;
}
get方法里面主要就是hash 和 getNode
1.1 hash(Object key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.1.1 hashCode
所有對(duì)象都會(huì)有也是必須有hashCode()方法的官辈,這也就是為什么HashMap的Key要求必須是對(duì)象的原因(不可用基本類型,int,long,等等)遍坟。當(dāng)然Value也是要求必須是對(duì)象的拳亿,為什么就待日后再說(shuō)。
這是Object類里面hashCode()方法愿伴。
public native int hashCode();
一個(gè)不常見(jiàn)的關(guān)鍵詞 native肺魁,使用native關(guān)鍵字說(shuō)明這個(gè)方法是原生函數(shù),也就是這個(gè)方法是用C/C++語(yǔ)言實(shí)現(xiàn)的隔节。鹅经。。后面的不想說(shuō)啦怎诫,既然是C系列那么就不在在下的關(guān)注之中了瘾晃,我們回到源碼Object對(duì)這個(gè)函數(shù)的描寫(xiě)
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* 返回對(duì)象的哈希值,這個(gè)方法是為了給哈希表提供幫助的(也就是這次講的HashMap)
* <p>
* The general contract of {@code hashCode} is:
* 約束是
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* 在一個(gè)java應(yīng)用執(zhí)行中幻妓,對(duì)同一個(gè)對(duì)象多次調(diào)用 hashCode()方法蹦误,必須返回相同的整形,
* 這個(gè)前提是在對(duì)象的比較中肉津,沒(méi)有任何信息被修改.相同程序在多次分別執(zhí)行時(shí)强胰,是不需要相同的
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* 如果兩個(gè)對(duì)象調(diào)用equals()方法是相等的,那么調(diào)用hashCode方法的返回也是相同的
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* 兩個(gè)不相等的對(duì)象妹沙,(不)要求hashCode相同偶洋,但是程序猿需要知道,給不相等對(duì)象提供不同的
* hash值有利于hash表的查詢
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java? programming language.)
* 呵呵距糖,Object自己的hashCode方法在不同的對(duì)象上返回不同的整形
*(這是依賴內(nèi)部地址轉(zhuǎn)換為整形來(lái)實(shí)現(xiàn)玄窝,但是我們重寫(xiě)這個(gè)方法的時(shí)候不要求這樣~)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
1.1.2 >>>
Object.hashCode()已經(jīng)返回了這個(gè)對(duì)象的hash值,那么為什么HashMap里面還有有個(gè)hash方法呢悍引?
(h = key.hashCode()) ^ (h >>> 16)
這個(gè)操作的高度概括就是讓Key的哈希值高位參與運(yùn)算哆料。
為什么要高位參與運(yùn)算呢?
我們算出來(lái)的哈希值是一個(gè)int型吗铐,2進(jìn)制32位帶符號(hào)的int是-2147483648~2147483648东亦,其實(shí)很難會(huì)發(fā)生碰撞(上面也說(shuō)到了,Object提供的hashCode方法算出來(lái)不同對(duì)象的哈希值是不會(huì)有重復(fù)的),如果我們直接使用哈希值作為數(shù)組下標(biāo)訪問(wèn)的話典阵,內(nèi)存是吃不消的奋渔,所以這個(gè)算出來(lái)的哈希值之后會(huì)和 當(dāng)前HashMap的大小做取模運(yùn)算得到的余數(shù) 當(dāng)前HashMap的大小-1 做與運(yùn)算作為下標(biāo)
p = tab[index = (n - 1) & hash]
這一段是之后put方法中使用獲得下標(biāo)的語(yǔ)句,這個(gè)我們之后再講壮啊,根據(jù)上面的代碼嫉鲸,我們?cè)倏础ashMap的默認(rèn)大小是2^4=16,換算成32位的樣式就是
0000 0000 0000 0000 0000 0000 0001 0000 -->16
0000 0000 0000 0000 0000 0000 0000 1111 -->15
當(dāng)我們拿著15去做與運(yùn)算的時(shí)候
從0000 0000 0000 0000 0000 0000 0000 0000
到1111 1111 1111 1111 1111 1111 1111 0000
所有低四位為0的對(duì)象歹啼,在這個(gè)HashMap中都會(huì)存到下標(biāo)為0的對(duì)象里面玄渗,不考慮擴(kuò)容等問(wèn)題,這樣的HashMap(1.8之前)他的查找效率就跟鏈表一樣狸眼,即使1.8將鏈表大小超過(guò)8的鏈表轉(zhuǎn)為紅黑樹(shù)藤树,這也不是HashMap的設(shè)計(jì)初衷。
但是我們將算出來(lái)的哈希值右移16位后取異或拓萌,那么就當(dāng)前大小16的HashMap來(lái)說(shuō)岁钓,參與運(yùn)算的就不只是0 ~ 3位,是0 ~ 3和16 ~ 19位共同的計(jì)算結(jié)果微王,這個(gè)操作使我們的分布更加均勻屡限。
而我們的HashMap的大小默認(rèn)值是16也就是2^4,而有符號(hào)int標(biāo)志范圍炕倘。
順便一提的是這也是為什么我們HashMap的大小必須2的冪次方钧大,因?yàn)檫@樣,大小-1正好是地位掩碼罩旋。
1.2 getNode(int hash,Object key)
以上拓型,我們知道了在HashMap中哈希值的計(jì)算方式,下面我們要討論的是取到hash值之后瘸恼,我們要怎么去取到具體的Value
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
我們已經(jīng)知道了劣挫,HashMap,最底層的數(shù)據(jù)結(jié)構(gòu)是Node<K,V>[],其實(shí)這段代碼蠻好懂的东帅,就是幾個(gè)條件压固,唯一值得注意的地方就是,我們?cè)诰唧w判斷的時(shí)候靠闭,首先判斷((k = e.key) == key),其次我們還要判斷(key != null && key.equals(k))帐我,==和equals的區(qū)別就不贅述了。
從一個(gè)get方法我們也可以看出愧膀,如果我們想用自己新建的一個(gè)類作為HashMap的key,我們一定要正確的重寫(xiě)這個(gè)類的hashCode()和equals()方法拦键,不然最終的結(jié)果可能并不是我們想要的。
這段代碼里面還有與之前JDK版本相比最大的區(qū)別就是引入了紅黑樹(shù)檩淋,這段代碼也可以看到芬为,如果我們這個(gè)節(jié)點(diǎn)是TreeNode的話萄金,我們會(huì)使用TreeNode的getTreeNode(hash,key)的方法獲得我們想要的Node節(jié)點(diǎn),如果不是TreeNode的話媚朦,我們會(huì)遍歷鏈表氧敢。
今天先到這里~
我是加薪貓
技術(shù)是加薪的前提,生活是加薪的意義
技術(shù)等于生活