1. 程式人生 > >源碼分析:HashMap

源碼分析:HashMap

正常 ava 這一 常見 簡單的 maximum hold seed 一點

寫在前面

作為以key/value存儲方式的集合,HashMap可以說起到了極大的作用。因此關於HashMap,我們將著重使用比較大的篇幅。

接下來會用到的幾個常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int MAXIMUM_CAPACITY = 1 << 30;

先簡單過一下,HashMap的思路

我們put的key/value會被封裝成一個叫做Entry的內部類。這個Entry由一個變量名為table數組管理。我們每次put會通過一系列的計算,計算一個table數組的index下標用於放Entry,如果出現hash沖突使用鏈表法解決。get時,可以理解是一個反向的put過程。


put(K key, V value)

1、初始化

if (table == EMPTY_TABLE) {
      //threshold變量在初始化的時候使用DEFAULT_INITIAL_CAPACITY(16)初始化
      inflateTable(threshold);
}

private void inflateTable(int toSize) {
      //計算我們table數組應該有多大(初始化是16)
      int capacity = roundUpToPowerOf2(toSize);
      //重新給threshold賦值:數組容量*0.75
      threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
      //初始化數組
      table = new Entry[capacity];
      //根據註釋:應用於推遲初始化(暫時不做深究)
      initHashSeedAsNeeded(capacity);
}

走到這一步,相當於我們的第一次put的初始化過程完成。那麽接著讓我們看下一步操作。


2、key為null

當然這一步的前面還有一個key為null的情況。因為實在是太直白,就不單獨展開,代碼如下:

if (key == null)
    return putForNullKey(value);

private V putForNullKey(V value) {
        //在0位置上的Entry鏈上遍歷,如果已存在key為null的Entry,替換value,並返回老的value
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //0位置如果沒有存在Entry,直接正常add我們這個key為null的Entry
        addEntry(0, null, value, 0);
        return null;
}

這裏我們可以得到一個信息,key為null的Entry會放在table[0]的位置上。


3、index下標計算

走到這一步,我們所要做的就是先計算我們這個key/value應該放在table的那個位置。也就是說,在真正包裝成Entry之前,我們需要確定這個Entry的應該再哪個坑裏。

計算hash值的hash方法如下:

//這個方法,的確是沒有太怎麽看明白是怎麽計算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();

        //註釋翻譯:該函數確保在每個數組位置上僅以恒定倍數不同的散列碼,具有有限數量的沖突(在默認加載因子下約為8)。
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

計算完hash之後,就是通過hash計算我們Entry應該在那個下標中:

//這倆個參數一個是上文計算的hash,一個是table的length
static int indexFor(int h, int length) {
    return h & (length-1);
}

走到這一步我們Entry該放在哪個位置已經明確了,這裏有很多位運算...根據效果來看,這樣的計算方式保證了,Entry更少的沖突。


4、插入Entry

既然上訴2的過程已經確定了插入位置,那麽毫無疑問,我們該插入這個
Entry了。

1、重復key處理

既然插入,那麽勢必有可能遇到重復問題,比如說,我們插入同一個key。這裏就是一個比較常見的問題,Map可不可以使用重復key,或者Map怎樣處理重復key的問題。

//如果index有存在的Entry,很簡單for循環遍歷這條鏈
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))) {
        //直接替換value,並且返回老的value
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
    }
}

因此關於key重復的問題,我們就可以得到答案。Map的操作是替換舊的value並返回老的value。

2、擴容

上訴步驟我們處理的key重復的問題。那麽接下來,就是Map的擴容過程。這裏會用到一個變量threshold,我們知道初始化table之後,這個變量 = 數組長度*0.75。記住這個值,它就是擴容的閾值。

擴容的

void addEntry(int hash, K key, V value, int bucketIndex) {
  //當前put進來的size>=threshold並且index下標不為空
  if ((size >= threshold) && (null != table[bucketIndex])) {
        //擴容2倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        //重新計算index
        bucketIndex = indexFor(hash, table.length);
   }
   //不屬於擴容的範疇
   createEntry(hash, key, value, bucketIndex);
}

3、創建並添加


void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

首先獲取index下標下的Entry,我們明白這裏的e有可能為空。(而這裏沒有對null這種情況進行判斷,也就是說這裏為不為空都無所謂)
拿到e之後,進行new Entry()把e,以及hash,key,value傳了進來。
這裏我們看一個e,放在了哪裏?


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;
            //我們重復的Entry直接被放在了next上。
            next = n;
            key = k;
            hash = h;
        }
        //省略部分內容
}

這裏說明一個什麽問題?那就是當hash沖突之後,我們的Entry是在鏈表的頭還是尾。根據代碼來看,很明顯是在鏈表的頭,這也說明了,為什麽e為null這種情況沒有做特別處理。

我們對put的分析就到此為止,既然分析了put,那麽接下來就是get。


get(Object key)

1、處理key為null的情況

if (key == null)
    return getForNullKey();

private V getForNullKey() {
    //如果當前size為0,可以就沒有對應的value
    if (size == 0) {
        return null;
    }
    //上文中我們分析到,put,key為null的value時,是放在table[0]上,那麽取的時候,肯定也是去table[0]上去取。
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

關於key為null的情況,其實我們也能看出,比較簡單明了。接下來就是key不為null的情況。


2、key不為null


    final Entry<K,V> getEntry(Object key) {
        //判空
        if (size == 0) {
            return null;
        }
        //通過key計算一個hash值。這裏的key為null的判斷,其實多此一舉。
        int hash = (key == null) ? 0 : hash(key);
        //反向計算index,也就是對應這個key的Entry在數組的哪個下標中。
        //因為我們put的時候知道,有可能index是會重復的。因此這裏使用了一個循環去遍歷這條鏈。如果hash相同,且key相同,那麽就是我們要找的value,返回即可。
        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;
    }

代碼思路比較的明確,其實就是一個反向的put過程。我們先通過key計算hash,然後計算對應Entry在數組中的index,然後遍歷對應鏈,找出匹配的value即可。

get方法我們可以看出,是比較簡單的。


尾聲

分析完put/get其實基本上HashMap就梳理完畢。
這裏我們進行一點總結:

  • 1、put時,key可以為空。並且放在table[0]的這個位置
  • 2、擴容策略是,size>=當前容量*0.75並且當前table[index]不為null。擴容大小為2倍。
  • 3、JDK1.7的HashMap使用鏈表法解決沖突,並且新插入的Entry是在鏈表的表頭。

源碼分析:HashMap