1. 程式人生 > 其它 >P5110 塊速遞推

P5110 塊速遞推

HashMap原始碼解析

前言

  • 本文是關於HashMap的原始碼解析
  • 將講解JDK1.7 & 1.8的HashMap
  • 會將兩個版本作為對比來進行解析和學習

原始碼解析

JDK 1.7

  • 基本引數
// HashMap的初始容量 
static final int DEFAULT_INITIAL_CAPACITY = 1 4; 
// HashMap的最大容量 
static final int MAXIMUM_CAPACITY = 1 30;
// HashMap的擴容因子 
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
// 鍵值對的個數 
transient int size; 
  • 建構函式
/** * initialCapacity 就是初始容量
* loadFactor 就是它的負載因子
*/
public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap(Map<? extends K, ? extends V> m) {
public HashMap() 

好了,現在我們把基本的一些引數方法都列了出來,讓我們開始分析。 我們先一步步來。從我們最開始 Map userMap = new HashMap();點進原始碼,找到他的構造方法,裡面方法體長這個樣子: 這裡面還呼叫的另一個構造方法,我們繼續跟進: 在這裡,它傳入了兩個值,initialCapacity

是初始容量 loadFactor是負載因子,哦吼,有丶意思,讓我們來研究一下。

/** 在這段程式碼裡 
*/ 
public HashMap(int initialCapacity, float loadFactor) {
// 先判斷初始容量是不是0, HashMap的最大容量(這裡最大容量是2的30次方 
// 如果結果為true的話 就讓傳進來的輸 === 最大容量
  if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; 
// 判斷 傳進來的負載因子 是否合法 或者>0, 結果為true 就拋錯 
if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor; threshold = initialCapacity; init();
}

這就是建構函式的分析了 接下來我們應該看看put方法,看看HashMap到底是怎樣存值的

public V put(K key, V value) {
    // 先判斷了table是否為空,為空才去初始化
    // 這裡有一個知識點,只有table為空的時候在去進行初始化。 所以有的面試官會問:
    // HashMap是不是在new的時候就進行了初始化,這裡可以很明顯的看到,HashMap在第一個put的時候才去進行的初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 大家都知道 HashMap是可以存null值的,null也可以當作鍵值去存值
    // 這裡就是HashMap可以存null值的具體實現了
    // 這段程式碼詳細寫了 key==null時
    // 會將value值放入首位
    // 這時候如果再傳入null值 會將舊值替換成新值
    if (key == null) return putForNullKey(value);
    // 計算hash
      int hash = hash(key);
      int i = indexFor(hash, table.length);
      for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
        //這裡是判斷HashMap裡是否已經有了這個值,如果有了,則用新值替換掉舊值,這也就是說,為什麼 map裡的key不能重複
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        modCount++; 
// 將key value傳入map addEntry(hash, key, value, i);
        return null;
    }

這裡額外講一下怎麼計算索引,也就是value存放的地方 在put方法中:計算完雜湊後,根據雜湊和陣列長度去計算對應的索引,也就是這個key應該在數組裡哪裡儲存。 我們發現每次put的時候都需要重新計算hash。HashMap的資料結構是陣列+連結串列,陣列特點是查詢快增刪慢,連結串列的特點是增刪快,查詢慢。我們用HashMap肯定主要是為了查詢的呀。所以從應用的角度考慮,我們肯定希望這些元素能均勻的分佈在陣列的不同格子裡,這樣做查詢的時候就會快。 對於計算出來的Hash值,不管是二進位制還是字母還是啥啥啥,反正能轉換為二進位制就是了,能轉成二進位制那是否就能轉成十進位制,反正你是個數字,對吧,每個key的hash不一樣,那麼對陣列長度取餘是否就算是平均分了。這時候我們先看一眼計算hash的方法
這裡發現好多的左移右移的位運算子。目的是通過各種右移能夠讓高位也參與運算,最大化的避免高位相同低位不同分到同一個索引。

這裡計算索引講完了,接下來我們講一下儲存。廢話不多說,上程式碼

void addEntry(int hash, K key, V value, int bucketIndex) {
        //當前的元素個數大於等於擴容閾值的時候,並且分配給新元素的這個位置以及有值,則擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //擴容為原來的陣列長度乘2
            resize(2 * table.length);
            //如果key=null,則hash為0
            hash = (null != key) ? hash(key) : 0;
            //根據新陣列長度重新計算索引
            bucketIndex = indexFor(hash, table.length);
        }
        // 儲存
        createEntry(hash, key, value, bucketIndex);
    }

這裡需要講的是,HashMap的初始值為16,這個16是一個經驗值,規定HashMap的容量必須為2的n次方,初始太小了要頻繁擴容,太大了又浪費,所以為了平衡選擇16。
HashMap的第一次擴容發生在容量達到12的時候,16*0.75=12。

JDK1.8 HashMap

1.8相比1.7 在基本屬性上多了兩個

// 樹化閾值
static final int TREEIFY_THRESHOLD = 8;
// 非樹化閾值
static final int UNTREEIFY_THRESHOLD = 6;

1.7的資料結構是:陣列+連結串列
1.8的資料結構是:陣列+連結串列+紅黑樹