HashMap原理機制詳解

大家在求職應聘java開發崗時,想必會經常被面試官問到HashMap是怎麼實現的問題,本文

通過jdk源碼來簡析HashMap的實現機制。

首先,HashMap繼承了AbstractMap,並實現了Map、Cloneable和Serializable接口,這裏不

作闡述。在Eclipse裏查看源碼,可得如下類結構截圖。


首先來看成員變量,注意到有一個Entry類型的數組table,這個其實就是真正用來存儲我們

的數據的地方,其中Entry是HashMap的一個靜態內部類,它包含一個next引用(源代碼如

下),其實它就是一個簡單的鏈表結構

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

到這裏,我們其實可以知道HashMap是怎麼回事了,它底層是利用數組(還記得那個table麼?)實現,數組每一個元素本身相當於一

個鏈表,我們的數據就存在這些個鏈表的域中!我們知道數組存取速度快,鏈表增刪方便,HashMap是在存取和增刪這兩方面取了一

個折中!結構圖如下


是不是很簡單?

我們再看前三個final變量,根據名字就知道,分別是HashMap的默認初始容量、最大容量

、默認負載因子。不錯,HashMap也是有最大容量的(2的30次方),當然平時我們都沒

機會讓它爆掉。爲了節省空間,HashMap一開始只是申請一個相對較小的空間(一般就是

默認的容量數值這裏是2的16次方)。對於負載因子,這裏有必要提一下,它和HashMap

的容量有關,比如負載因子爲0.75,表示12單位的空間只存儲12*0.75=8個單位,這個數

字8其實也就是threshold的意義!而size變量表示真個map存儲的鍵值對的個數,在後文

對put函數講解得時候會說到HashMap的擴容方式,它的判斷標準也是根據這個將size和

threshold變量進行大小比較。

再看它的構造函數,其中有四個構造函數,我們平時用的最多是第三個無參構造函數,

其源碼如下:

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

這裏做一些基本的初始化工作,注意threshold等於什麼!這在之前已經說過。另外,

init()是個空方法,這裏不太清楚原因,有知道的網友,多多指教啊!其它三個構

造函數其實最終都是調用調用第一個構造函數,只是使用傳入的參數進行初始化。

接下來,看看兩個最具代表性的方法:put、get

對於添加鍵值對對象的put方法:

   public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

該方法,首先判斷key是否爲null,如果null則調用putForNullKey方法,查看源碼發現putForNullKey方法默認

將Null對應的value放在table[0]位置所在的entry鏈表上(因爲有的key會映射到該

位置),所以HashMap是支持null爲key的!然後,通過就是計算key對應的hash

值,hash和indexFor函數如下:

 static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

沒啥特別要說的,大底知道是通過無符號位移操作再與table的長度進行與操作,且不提。在put函數的最後有一行

 addEntry(hash, key, value, i);

也就是添加元素到對應的鏈表中,其源碼爲:

 void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }
看到其中的if 語句了麼? 當元素越來越多時,hashmap的擴容方式爲擴大一倍!

再接着,通過k的hashcode和k值判斷是否衝突!如果衝突則將

value覆蓋,並返回覆蓋之前的value,當然,沒有衝突和發生衝突的key值爲

null時都返回null (0.0, 別暈)

接下來,看看get方法:

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

同put一樣,先判斷key值是否爲null。 然後通過計算hashcode形成對table的一個映射,找到所在的entry鏈表,

然後就是對該鏈表進行簡單的遍歷,蠻簡單,不多提了!

最後提一下,table數組是聲明爲transient的,這可以防止HashMap被序列化。

另外,HashMap還有很多內部iterator接口的實現類,其主要是對HashMap

提供不需要了解內部結構就可以進行訪問的目的,這個可以參考iterator設

計模式 。










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章