天天看點

HashMap深度解析(一)

 hashmap可以說是java中最常用的集合類架構之一,是java語言中非常典型的資料結構,我們總會在不經意間用到它,很大程度上友善了我們日常

開發。在很多java的筆試題中也會被問到,最常見的,“hashmap和hashtable有什麼差別?”,這也不是三言兩語能說清楚的,這種筆試題就

是考察你來筆試之前有沒有複習功課,随便來個快餐式的複習就能給出簡單的答案。

 hashmap計劃寫兩篇文章,一篇是hashmap工作原理,也就是本文,另一篇是多線程下的hashmap會引發的問題。這一年文章寫的有點少,工

作上很忙,自己業餘時間也做點東西,就把部落格的時間占用了,以前是力保一周一篇文章,有點給自己任務的意思,搞的自己很累,文章品質也不高,有時候寫技術

文章也是需要靈感的,為了舉一個例子可能要絞盡腦汁,為了一段代碼可能要驗證好多次,現在想通了,有靈感再寫,需要一定的積累,才能把自己了解的知識點總

結歸納成文章。

       言歸正傳,了解hashmap之前,我們需要知道object類的兩個方法hashcode和equals,我們先來看一下這兩個方法的預設實作:

HashMap深度解析(一)

/** jni,調用底層其它語言實作 */  

public native int hashcode();  

/** 預設同==,直接比較對象 */  

public boolean equals(object obj) {  

    return (this == obj);  

}  

       equals方法我們太熟悉了,我們經常用于字元串比較,string類中重寫了equals方法,比較的是字元串值,看一下源碼實作:

HashMap深度解析(一)

public boolean equals(object anobject) {  

    if (this == anobject) {  

        return true;  

    }  

    if (anobject instanceof string) {  

        string anotherstring = (string) anobject;  

        int n = value.length;  

        if (n == anotherstring.value.length) {  

            char v1[] = value;  

            char v2[] = anotherstring.value;  

            int i = 0;  

            // 逐個判斷字元是否相等  

            while (n-- != 0) {  

                if (v1[i] != v2[i])  

                        return false;  

                i++;  

            }  

            return true;  

        }  

    return false;  

       重寫equals要滿足幾個條件:

自反性:對于任何非空引用值 x,x.equals(x) 都應傳回 true。 

對稱性:對于任何非空引用值 x 和 y,當且僅當 y.equals(x) 傳回 true 時,x.equals(y) 才應傳回 true。 

傳遞性:對于任何非空引用值 x、y 和 z,如果 x.equals(y) 傳回 true,并且 y.equals(z) 傳回 true,那麼 x.equals(z) 應傳回 true。 

一緻性:對于任何非空引用值 x 和 y,多次調用 x.equals(y) 始終傳回 true 或始終傳回 false,前提是對象上 equals 比較中所用的資訊沒有被修改。 

對于任何非空引用值 x,x.equals(null) 都應傳回 false。 

       object 類的 equals 方法實作對象上差别可能性最大的相等關系;即,對于任何非空引用值 x 和 y,當且僅當 x

和 y 引用同一個對象時,此方法才傳回 true(x == y 具有值 true)。 當此方法被重寫時,通常有必要重寫 hashcode

方法,以維護 hashcode 方法的正常協定,該協定聲明相等對象必須具有相等的哈希碼。

       下面來說說hashcode方法,這個方法我們平時通常是用不到的,它是為哈希家族的集合類架構(hashmap、hashset、hashtable)提供服務,hashcode 的正常協定是:

在 java 應用程式執行期間,在同一對象上多次調用 hashcode 方法時,必須一緻地傳回相同的整數,前提是對象上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一緻。 

如果根據 equals(object) 方法,兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashcode 方法都必須生成相同的整數結果。 

下情況不 是必需的:如果根據 equals(java.lang.object) 方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用

hashcode 方法必定會生成不同的整數結果。但是,程式員應該知道,為不相等的對象生成不同整數結果可以提高哈希表的性能。

 當我們看到實作這兩個方法有這麼多要求時,立刻淩亂了,幸好有ide來幫助我們,eclipse中可以通過快捷鍵alt+shift+s調出快捷菜單,

選擇generate hashcode() and

equals(),根據業務需求,勾選需要生成的屬性,确定之後,這兩個方法就生成好了,我們通常需要在javabean對象中重寫這兩個方法。

 好了,這兩個方法介紹完之後,我們回到hashmap。hashmap是最常用的集合類架構之一,它實作了map接口,是以存儲的元素也是鍵值對映射的

結構,并允許使用null值和null鍵,其内元素是無序的,如果要保證有序,可以使用linkedhashmap。hashmap是線程不安全的,下篇

文章會讨論。hashmap的類結構如下:

<dl></dl>

<dt></dt>

所有已實作的接口:

<dd></dd>

直接已知子類:

       hashmap中我們最長用的就是put(k, v)和get(k)。我們都知道,hashmap的k值是唯一的,那如何保證唯一性呢?我們首先想到的是用equals比較,沒錯,這樣可以實作,但随着内部元素的增多,put和get的效率将越來越低,這裡的時間複雜度是o(n),假如有1000個元素,put時需要比較1000次。實際上,hashmap很少會用到equals方法,因為其内通過一個哈希表管理所有元素,哈希是通過hash單詞音譯過來的,也可以稱為散清單,雜湊演算法可以快速的存取元素,當我們調用put存值時,hashmap首先會調用k的hashcode方法,擷取哈希碼,通過哈希碼快速找到某個存放位置,這個位置可以被稱之為bucketindex,通過上面所述hashcode的協定可以知道,如果hashcode不同,equals一定為false,如果hashcode相同,equals不一定為true。是以理論上,hashcode可能存在沖突的情況,有個專業名詞叫碰撞,當碰撞發生時,計算出的bucketindex也是相同的,這時會取到bucketindex位置已存儲的元素,最終通過equals來比較,equals方法就是哈希碼碰撞時才會執行的方法,是以前面說hashmap很少會用到equals。hashmap通過hashcode和equals最終判斷出k是否已存在,如果已存在,則使用新v值替換舊v值,并傳回舊v值,如果不存在

,則存放新的鍵值對&lt;k, v&gt;到bucketindex位置。文字描述有些亂,通過下面的流程圖來梳理一下整個put過程。

HashMap深度解析(一)

       現在我們知道,執行put方法後,最終hashmap的存儲結構會有這三種情況,情形3是最少發生的,哈希碼發生碰撞屬于小機率事件。到目前為止,我們了解了兩件事:

hashmap通過鍵的hashcode來快速的存取元素。

當不同的對象hashcode發生碰撞時,hashmap通過單連結清單來解決,将新元素加傳入連結表表頭,通過next指向原有的元素。單連結清單在java中的實作就是對象的引用(複合)。

       來鑒賞一下hashmap中put方法源碼:

HashMap深度解析(一)

public v put(k key, v value) {  

    // 處理key為null,hashmap允許key和value為null  

    if (key == null)  

        return putfornullkey(value);  

    // 得到key的哈希碼  

    int hash = hash(key);  

    // 通過哈希碼計算出bucketindex  

    int i = indexfor(hash, table.length);  

    // 取出bucketindex位置上的元素,并循環單連結清單,判斷key是否已存在  

    for (entry&lt;k,v&gt; e = table[i]; e != null; e = e.next) {  

        object k;  

        // 哈希碼相同并且對象相同時  

        if (e.hash == hash &amp;&amp; ((k = e.key) == key || key.equals(k))) {  

            // 新值替換舊值,并傳回舊值  

            v oldvalue = e.value;  

            e.value = value;  

            e.recordaccess(this);  

            return oldvalue;  

    // key不存在時,加入新元素  

    modcount++;  

    addentry(hash, key, value, i);  

    return null;  

       到這裡,我們了解了hashmap工作原理的一部分,那還有另一部分,如,加載因子及rehash,hashmap通常的使用規則,多線程并發時hashmap存在的問題等等,這些會留在下一章說明。