Java集合系列之HashMap源碼分析,hashmap源碼

來源:互聯網
上載者:User

Java集合系列之HashMap源碼分析,hashmap源碼

前面我們已經分析了ArrayList和LinkedList這兩個集合,我們知道ArrayList是基於數組實現的,LinkedList是基於鏈表實現的。它們各自有自己的優劣勢,例如ArrayList在定位尋找元素時會優於LinkedList,而LinkedList在添加刪除元素時會優於ArrayList。而本篇介紹的HashMap綜合了二者的優勢,它的底層是基於雜湊表實現的,如果不考慮雜湊衝突的話,HashMap在增刪改查操作上的時間複雜度都能夠達到驚人的O(1)。我們先看看它所基於的雜湊表的結構。

從中可以看到,雜湊表是由數組和鏈表共同構成的一種結構,當然是一個不好的樣本,一個好的雜湊函數應該要盡量平均元素在數組中的分布,減少雜湊衝突從而減小鏈表的長度。鏈表的長度越長,意味著在尋找時需要遍曆的結點越多,雜湊表的效能也就越差。接下來我們來看下HashMap的部分成員變數。

//預設初始容量static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//預設最大容量static final int MAXIMUM_CAPACITY = 1 << 30;//預設載入因子, 指雜湊表可以達到多滿的尺度static final float DEFAULT_LOAD_FACTOR = 0.75f;//空的雜湊表static final Entry<?,?>[] EMPTY_TABLE = {};//實際使用的雜湊表transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//HashMap大小, 即HashMap儲存的索引值對數量transient int size;//索引值對的閾值, 用於判斷是否需要擴增雜湊表容量int threshold;//載入因子final float loadFactor;//修改次數, 用於fail-fast機制transient int modCount;//使用替代雜湊的預設閥值static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//隨機的雜湊種子, 有助於減少雜湊碰撞的次數transient int hashSeed = 0;

由成員變數中看到,HashMap預設的初始容量為16,預設的載入因子是0.75。而threshold是集合能夠儲存的索引值對的閥值,預設是初始容量*載入因子,也就是16*0.75=12,當索引值對要超過閥值時,意味著這時候的雜湊表已處於飽和狀態,再繼續添加元素就會增加雜湊衝突,從而使HashMap的效能下降。這時會觸發自動擴容機制,以保證HashMap的效能。我們還可以看到雜湊表其實就是一個Entry數組,數組存放的每個Entry都是單向鏈表的頭結點。這個Entry是HashMap的靜態內部類,來看看Entry的成員變數。

static class Entry<K,V> implements Map.Entry<K,V> {  final K key;   //鍵  V value;     //值  Entry<K,V> next; //下一個Entry的引用  int hash;     //雜湊碼    ...        //省略下面代碼}

一個Entry執行個體就是一個索引值對,裡麵包含了key和value,同時每個Entry執行個體還有一個指向下一個Entry執行個體的引用。為了避免重複計算,每個Entry執行個體還存放了對應的Hash碼。可以說Entry數組就是HashMap的核心,所有的操作都是針對這個數組來完成的。由於HashMap源碼比較長,不可能面面俱到的介紹它的所有方法,因此我們只抓住重點來進行介紹。接下來我們將以問題為導向,針對下面幾個問題深入探究HashMap的內部機制。

1. HashMap在構造時做了哪些操作?

//構造器, 傳入初始化容量和載入因子public HashMap(int initialCapacity, float loadFactor) {  if (initialCapacity < 0) {    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);  }  //如果初始化容量大於最大容量, 就把它設為最大容量  if (initialCapacity > MAXIMUM_CAPACITY) {    initialCapacity = MAXIMUM_CAPACITY;  }  //如果載入因子小於0或者載入因子不是浮點數, 則拋出異常  if (loadFactor <= 0 || Float.isNaN(loadFactor)) {    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);  }  //設定載入因子  this.loadFactor = loadFactor;  //閾值為初始化容量  threshold = initialCapacity;  init();}void init() {}

所有的構造器都會調用到這個構造器,在這個構造器中我們看到除了對參數做一些校正之外,它就做了兩件事,設定載入因子為傳入的載入因子,設定閥值為傳入的初始化大小。而init方法是空的,啥也沒做。注意,這時候並沒有根據傳入的初始化大小去建立一個Entry數組哦。那在什麼時候再去建立數組呢?繼續往下看。

2. HashMap添加索引值對時會進行什麼操作?

//放置key-value索引值對到HashMap中public V put(K key, V value) {  //如果雜湊表沒有初始化就進行初始化  if (table == EMPTY_TABLE) {    //初始化雜湊表    inflateTable(threshold);  }  if (key == null) {    return putForNullKey(value);  }  //計算key的hash碼  int hash = hash(key);  //根據hash碼定位在雜湊表的位置  int i = indexFor(hash, table.length);  for (Entry<K,V> e = table[i]; e != null; e = e.next) {    Object k;    //如果對應的key已經存在, 就替換它的value值, 並返回原先的value值    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就添加Entry到HashMap中  addEntry(hash, key, value, i);  //添加成功返回null  return null;}

看到,在添加索引值對時會先檢查雜湊表是否是個空表,如果是空表就進行初始化。之後再進行後續操作,調用雜湊Function Compute傳入的key的Hash碼。根據hash碼定位到Entry數組的指定槽位,然後對該槽位的單向鏈表進行遍曆,如果傳入的已經存在了就進行替換操作,否則就建立一個Entry添加到雜湊表中。

3. 雜湊表是怎樣初始化的?

//初始化雜湊表, 會對雜湊表容量進行膨脹, 因為有可能傳入的容量不是2的冪private void inflateTable(int toSize) {  //雜湊表容量必須是2的次冪  int capacity = roundUpToPowerOf2(toSize);  //設定閥值, 這裡一般是取capacity*loadFactor  threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  //建立指定容量的雜湊表  table = new Entry[capacity];  //初始化雜湊種子  initHashSeedAsNeeded(capacity);}

上面我們知道,在構造HashMap時不會建立Entry數組,而是在put操作時檢查當前雜湊表是否是個空表,如果是空表就調用inflateTable方法進行初始化。上面貼出了這個方法的代碼,可以看到方法內部會重新計算Entry數組的容量,因為在構造HashMap時傳入的初始化大小可能不是2的冪,因此要將這個數轉換成2的冪再去根據新的容量建立Entry數組。初始化雜湊表時再次重新設定閥值,閥值一般是capacity*loadFactor。此外,在初始化雜湊表時還會去初始化雜湊種子(hashSeed),這個hashSeed用於最佳化雜湊函數,預設為0是不使用替代雜湊演算法,但是也可以自己去設定hashSeed的值,以達到最佳化效果。具體下面會講到。

4. HashMap在什麼時候判斷是否要擴容,以及它是怎樣擴容的?

//添加Entry方法, 先判斷是否要擴容void addEntry(int hash, K key, V value, int bucketIndex) {  //如果HashMap的大小大於閥值並且雜湊表對應槽位的值不為空白  if ((size >= threshold) && (null != table[bucketIndex])) {    //因為HashMap的大小大於閥值, 表明即將發生雜湊衝突, 所以進行擴容    resize(2 * table.length);    hash = (null != key) ? hash(key) : 0;    bucketIndex = indexFor(hash, table.length);  }  //在這裡表明HashMap的大小沒有超過閥值, 所以不需要擴容  createEntry(hash, key, value, bucketIndex);}//對雜湊表進行擴容void resize(int newCapacity) {  Entry[] oldTable = table;  int oldCapacity = oldTable.length;  //如果當前已經是最大容量就只能增大閥值了  if (oldCapacity == MAXIMUM_CAPACITY) {    threshold = Integer.MAX_VALUE;    return;  }  //否則就進行擴容  Entry[] newTable = new Entry[newCapacity];  //遷移雜湊表的方法  transfer(newTable, initHashSeedAsNeeded(newCapacity));  //將當前雜湊表設定為新的雜湊表  table = newTable;  //更新雜湊表閾值  threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}

在調用put方法添加一個索引值對時,如果集合中沒有存在的key就去調用addEntry方法建立一個Entry。看到上面貼出的addEntry代碼,在建立一個Entry之前會先判斷當前集合元素的大小是否超過了閥值,如果超過了閥值就調用resize進行擴容。傳入的新的容量是原來雜湊表的兩倍,在resize方法內部會建立一個容量為原先的2倍的Entry數組。然後將舊的雜湊表裡面的元素全部遷移到新的雜湊表,其中可能會進行再雜湊,根據initHashSeedAsNeeded方法計算的值來確定是否進行再雜湊。完成雜湊表的遷移之後,將當前雜湊表替換為新的,最後再根據新的雜湊表容量來重新計算HashMap的閥值。

5. 為什麼Entry數組的大小必須為2的冪?

 //返回雜湊碼對應的數組下標 static int indexFor(int h, int length) {   return h & (length-1); }

indexFor方法是根據hash碼來計算出在數組中對應的下標。我們可以看到在這個方法內部使用了與(&)操作符。與操作是對兩個運算元進行位元運算,如果對應的兩個位都為1,結果才為1,否則為0。與操作經常會用於去除運算元的高位值,例如:01011010 & 00001111 = 00001010。我們繼續回到代碼中,看看h&(length-1)做了些什麼。

已知傳入的length是Entry數組的長度,我們知道數組下標是從0開始計算的,所以數組的最大下標為length-1。如果length為2的冪,那麼length-1的二進位位後面都為1。這時h&(length-1)的作用就是去掉了h的高位值,只留下h的低位值來作為數組的下標。由此可以看到Entry數組的大小規定為2的冪就是為了能夠使用這個演算法來確定數組的下標。

6. 雜湊函數是怎樣計算Hash碼的?

//產生hash碼的函數final int hash(Object k) {  int h = hashSeed;  //key是String類型的就使用另外的雜湊演算法  if (0 != h && k instanceof String) {    return sun.misc.Hashing.stringHash32((String) k);  }  h ^= k.hashCode();  //擾動函數  h ^= (h >>> 20) ^ (h >>> 12);  return h ^ (h >>> 7) ^ (h >>> 4);}

hash方法的最後兩行是真正計算hash值的演算法,計算hash碼的演算法被稱為擾動函數,所謂的擾動函數就是把所有東西雜糅到一起,可以看到這裡使用了四個向右移位元運算。目的就是將h的高位值與低位值混合一下,以此增加低位值的隨機性。在上面我們知道定位元組的下標是根據hash碼的低位值來確定的。key的hash碼是通過hashCode方法來產生的,而一個糟糕的hashCode方法產生的hash碼的低位值可能會有很大的重複。為了使得hash碼在數組上映射的比較均勻,擾動函數就派上用場了,把高位值的特性糅合進低位值,增加低位值的隨機性,從而使散列分布的更加鬆散,以此提高效能。舉了個例子協助理解。

7. 替代雜湊是怎麼回事?

我們看到hash方法中首先會將hashSeed賦值給h。這個hashSeed就是雜湊種子,它是一個隨機的值,作用就是協助最佳化雜湊函數。hashSeed預設是0,也就是預設不使用替代雜湊演算法。那麼什麼時候使用hashSeed呢?首先需要設定開啟替代雜湊,在系統屬性中設定jdk.map.althashing.threshold的值,在系統屬性中這個值預設是-1,當它是-1的時候使用替代雜湊的閥值為Integer.MAX_VALUE。這也意味著可能你永遠也不會使用替代雜湊了。當然你可以把這個閥值設小一點,這樣當集合元素達到閥值後就會產生一個隨機的hashSeed。以此增加hash函數的隨機性。為什麼要使用替代雜湊呢?當集合元素達到你設定的閥值之後,意味著雜湊表已經比較飽和了,出現雜湊衝突的可能性就會大大增加,這時對再添加進來的元素使用更加隨機的散列函數能夠使後面添加進來的元素更加隨機的分布在散列表中。

註:以上分析全部基於JDK1.7,不同版本之間會有較大的改動,讀者需要注意。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.