1. 程式人生 > >Java基礎(三)HashMap原始碼剖析

Java基礎(三)HashMap原始碼剖析

關於HashMap,在網上看到了不少的好文章,萬花叢中過的過程中,我自己卻有了很大的感慨。

關於HashMap很多好的文章介紹,都是關注於HashMap的一個點,進行展開介紹。簡簡單單幾張圖,幾行文字介紹,就給你豁然開朗的感覺。

絕大多數的好文章,都是在圍繞著HashMap資料結構、儲存方式,在進行講解性介紹。

這些好文章,我看完之後,不免會產生很多的疑惑,本文就圍繞解決我的疑惑展開。

這裡順便說一下那麼想寫好HashMap,卻看上去不太好懂的文章,說實話,那些文章,其實我結合原始碼看,也給我很大收貨,但是為什麼說他文章寫的不太好呢?我想,是因為那些作者太想通過一篇文章,把HashMap介紹明白了。全面展開,反而處處得不到深入。

建議:想學習什麼深入原始碼、自己研究框架這種類似的東西,你一定要先老老實實先把原始碼跟一遍,然後再去看網上所謂的好文章和不太好的文章,不然,你就是在浪費時間。

HashMap的資料結構

HashMap是陣列、連結串列和雜湊表共同實現對資料的儲存,但是陣列和連結串列基本上是兩個極端。

陣列

陣列儲存區間是連續的,佔用記憶體記憶體較大。故空間複雜度很大,時間複雜度很小。陣列的特點:定址容易,插入和刪除困難

連結串列

連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大。連結串列特點:定址困難,插入和刪除容易

資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。

而HashMap則通過雜湊表,綜合陣列和連結串列兩者之所長,實現資料儲存。

我們在向HashMap中存取值的時候,我們會這樣做:

HashMap<String,String> map = new HashMap<String,String>();

//向map中存入一個值

map.put("A","123");

//根據key,取出map中值

map.get("A");

那在HashMap內部是怎麼實現的呢?偽程式碼表示的話,類似於下面:

// 儲存時:
int hash = key.hashCode();
int index = hash % Entry[].length;
Entry[index] = value;

// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];

但是,HashMap在原始碼中的實現,比上面的虛擬碼更加複雜。儲存時,得到key的hashCode值後,還會對key進行加工處理:

如果key是String,則使用stringHash32直接對key做hash;如果key不是String,則對key的hashCode,進行復雜的位運算,使得二進位制數中1的分佈更加均勻。這樣做的好處,就是儘量做到hash值不重複。

因為hash不重複的資料,在定址時,能夠通過key的hash值,一次性找到,而如果hash多次重複,則資料會以連結串列的形式儲存,根據key獲取值時,只能遍歷查詢效率極低的連結串列(不懂的話,先略過,一會兒再回頭來看)。

hash演算法原始碼如下:

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // 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);
    }

put

上面已經提到了連結串列,那HashMap內部是如何利用連結串列儲存的呢?

HashMap內部採用了Entry物件。Entry是一個靜態內部類。其屬性有:key、value、next和hash四個屬性。從屬性key、value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,HashMap的基礎是一個線性陣列,這個陣列就是Entry[],Map裡面的內容都儲存在Entry[]裡面。

Entry的資料結構如下(Entry中提供的內部類方法忽略):

key就是鍵值對中的key;

value就是鍵值對中的value;

next為指標,當使用雜湊演算法,算出來的key重複,但key不重複時,則使用新的entry物件資料,指向舊的資料;

hash則是根據雜湊演算法和key,算出來的hash值。

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

     Entry(int h, K k, V v, Entry<K,V> n) {
          value = v;
          next = n;
          key = k;
          hash = h;
     }

   ..........

}

下面直接分析原始碼吧。put 的原始碼如下:

//HashMap的put方法
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
	//如果key是null值,單獨處理(其實處理方式,與非null完全類似)
        if (key == null)
            return putForNullKey(value);
	//key不為null值,則對key做hash演算法(實際是對key的hashCode做hash演算法),根據key,得到hashCode值
        int hash = hash(key);
	//根據hashCode值,以及map中不衝突的值的數量,得到key所在的位置 AAA
        int i = indexFor(hash, table.length);
	//如果位置AAA上有值,則對Entry物件進行遍歷
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
		//判斷新增的key是否重複:如果entry位置上的key的hash值與新增的key的hash值相同,則新值替換久值,返回久值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
	
	//如果位置AAA上沒有值,則新增新的值(即沒有衝突的情況)
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

請先不要考慮key為null的情況,我們根據上面提到的hash演算法,根據鍵值對的key,算出它對應的hash值,然後根據這個hash值 與 已經儲存hash不衝突資料的長度,確定值的位置或大概位置。

get

明白了put,則get就比較簡單了,key不為null時,直接根據getEntry方法和key,獲取Entry物件。

public V get(Object key) {	//如果key為null,則按照null規則處理
        if (key == null)
            return getForNullKey();

	 //key!=null;呼叫getEntry方法
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

那getEntry是如何根據key,獲取其對應的value的呢?

比較簡單,還是根據key,拿到hash值,根據key的hash值與hash值不衝突的個數,獲取key的位置,拿到Entry陣列。

拿到Entry陣列之後,必須保證兩個條件,才能拿到key對應的value值:一、entry中的hash值與key的hash值必須相等;二、key與entry中的key的值相等。

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }

總結

總結起來,一共有以下幾點,需要你注意:

1、本文沒有考慮key為null的情況,HashMap的實現,是可以允許key為null的,大家自己可以看看原始碼,統一的方式,很簡單;

2、key雜湊值的確定。不同版本的JDK中,雜湊演算法可能不盡相同,但是它們的目的只有一個:就是要減少衝突。(為什麼減少衝突,我在文章最前面就提過,key的hash衝突的資料,需要以連結串列的形式,儲存為Entry物件陣列,衝突時,就要遍歷該查詢效率低下的連結串列);

3、如果key的hash衝突了,那麼如何進行儲存,如何確保根據key取value時能夠正確取到?這是大家容易迷糊地方,也是我之前比較迷糊地方。確定value 的值,必須根據如下兩個條件:一、entry中的hash值與key的hash值必須相等;二、key與entry中的key的值相等。這樣做,是為了排除key相同的情況。

例如put時,我們要確定entry陣列中,entry.hash值與value的hash值相等,並且,entry.key與key相等,則說明這是put進來了key相同的值,HashMap的實現是新值覆蓋原先的值;

get時,則同樣是判斷entry.hash值與key的hash值相等,並且,entry.key與key相等,來唯一確定value的值。確定唯一值的過程中,如果key的hash值有衝突,則要遍歷連結串列來實現唯一確定。

4、Entry陣列位置的確定。根據如下函式確定:h是key的hash值;length是Entry陣列的個數。

static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

比較好的幾篇文章推薦: