日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

HashMap源碼剖析

 一本正經(jīng)地胡鬧 2019-09-03

本文來自:我的博客,原文地址:https://blog.csdn.net/silentljh/article/details/80444216,轉(zhuǎn)載請注明。

HashMap可以說是Java中最常用的集合類框架之一,是Java語言中非常典型的數(shù)據(jù)結(jié)構(gòu),我們總會在不經(jīng)意間用到它,很大程度上方便了我們?nèi)粘i_發(fā)。

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

HashMap概述

  1. HashMap是一種基于哈希表實(shí)現(xiàn)的Map,它通過鍵的hashCode來快速的存取元素
  2. HashMap允許插入null鍵和null值,允許多條記錄的值為null,但只允許一條記錄的鍵為null
  3. HashMap不是線程安全的,在并發(fā)環(huán)境下,可能會引起死循環(huán)
  4. HashMap中的元素是無序的,無法保證遍歷時的順序是固定不變的
  5. HashMap在不考慮哈希碰撞的情況下,插入和查詢的時間復(fù)雜度可以達(dá)到O(1)

HashMap的數(shù)據(jù)結(jié)構(gòu)

       HashMap是基于哈希表實(shí)現(xiàn)的,哈希表是由數(shù)組和鏈表共同構(gòu)成的一種結(jié)構(gòu),其中數(shù)組保存著每個單向鏈表的頭結(jié)點(diǎn),鏈表保存著具有相同hash值的不同元素,它是用來解決哈希沖突(Hash Collision)的。一個好的哈希函數(shù)應(yīng)該盡量使元素在數(shù)組中均勻分布,減少哈希沖突,從而縮短鏈表的長度。鏈表的長度越長,意味著在查找時需要遍歷的結(jié)點(diǎn)越多,哈希表的性能也就越差。

        HashMap中的數(shù)組是一個Entry數(shù)組,數(shù)組存放的每個Entry都是單向鏈表的頭結(jié)點(diǎn)。HashMap使用Entry類來表示Key-Value型的元素,一個Entry對象就是一個鍵值對,里面包含了key,value,hash值以及指向下一個Entry對象的引用。    

  1. static class Entry<K, V> implements Map.Entry<K, V> {
  2. final K key;
  3. V value;
  4. Entry<K, V> next; // 下一個Entry對象的引用
  5. int hash; // 元素的hash, 其實(shí)就是key的hash值

HashMap源碼剖析

1、常量和屬性解析

  1. // 默認(rèn)初始化容量 16
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  3. // HashMap允許的最大容量 2^30
  4. static final int MAXIMUM_CAPACITY = 1 << 30;
  5. // 默認(rèn)的負(fù)載率 75%
  6. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  7. // 空的哈希表
  8. static final Entry<?, ?>[] EMPTY_TABLE = {};
  9. // 實(shí)際使用的哈希表
  10. transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
  11. // HashMap的大小,即存儲的key-value的數(shù)量
  12. transient int size;
  13. // 擴(kuò)容的閥值,當(dāng)HashMap的size達(dá)到閥值時,就開始擴(kuò)容 threshold=length*threshold
  14. int threshold;
  15. // 負(fù)載率
  16. final float loadFactor;
  17. // 修改次數(shù), 用于fail-fast機(jī)制
  18. transient int modCount;
  19. // 替代哈希使用的默認(rèn)擴(kuò)容閥值
  20. static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
  21. // 隨機(jī)的哈希種子, 有助于減少發(fā)生哈希碰撞的幾率
  22. transient int hashSeed = 0;

2、構(gòu)造方法

        HashMap提供了一下4種構(gòu)造方法, 但最終都會調(diào)用HashMap(int initialCapacity, float loadFactor)方法。如果使用無參構(gòu)造函數(shù)創(chuàng)建HashMap,其內(nèi)部是通過調(diào)用HashMap(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR)實(shí)現(xiàn)。下面我們來分析一下這個構(gòu)造方法。

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. // 如果初始容量小于0,則拋出異常
  3. if (initialCapacity < 0) {
  4. throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  5. }
  6. // 如果初始容量大于容量最大值,則使用最大值作為初始容量
  7. if (initialCapacity > MAXIMUM_CAPACITY) { initialCapacity = MAXIMUM_CAPACITY; }
  8. // 如果負(fù)載率小于等于0或負(fù)載率不是浮點(diǎn)數(shù),則拋出異常
  9. if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
  10. throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
  11. }
  12. // 設(shè)置負(fù)載率
  13. this.loadFactor = loadFactor;
  14. // 設(shè)置閥值為初始容量
  15. threshold = initialCapacity;
  16. // 空實(shí)現(xiàn), 交由子類實(shí)現(xiàn)
  17. init();
  18. }

        看完這個方法,我們發(fā)現(xiàn)這個構(gòu)造方法并沒有根據(jù)傳入的initialCapacity去新建一個Entry數(shù)組,此時的哈希表依然是一個空表。HashMap在構(gòu)造時不會新建Entry數(shù)組,而是在put操作時會先檢查當(dāng)前哈希表是否是個空表,如果是空表就調(diào)用inflateTable方法進(jìn)行初始化。上面貼出了這個方法的代碼,可以看到方法內(nèi)部會重新計(jì)算Entry數(shù)組的容量,因?yàn)樵跇?gòu)造HashMap時傳入的初始化大小可能不是2的冪,因此要將這個數(shù)轉(zhuǎn)換成2的冪再去根據(jù)新的容量新建Entry數(shù)組。初始化哈希表時再次重新設(shè)置閥值,閥值一般是capacity*loadFactor。此外,在初始化哈希表時還會去初始化hashSeed,這個hashSeed用于優(yōu)化哈希函數(shù),默認(rèn)為0是不使用替代哈希算法,但是也可以自己去設(shè)置hashSeed的值,以達(dá)到優(yōu)化效果。具體下面會講到。

3、put方法

  1. public V put(K key, V value) {
  2. // 如果哈希表沒有初始化就進(jìn)行初始化
  3. if (table == EMPTY_TABLE) {
  4. // 初始化哈希表
  5. inflateTable(threshold);
  6. }
  7. // 當(dāng)key為null時,調(diào)用putForNullKey方法,保存null于table的第一個位置中,這是HashMap允許為null的原因
  8. if (key == null) {
  9. return putForNullKey(value);
  10. }
  11. // 計(jì)算key的hash值
  12. int hash = hash(key);
  13. // 根據(jù)key的hash值和數(shù)組的長度定位到entry數(shù)組的指定槽位
  14. int i = indexFor(hash, table.length);
  15. // 獲取存放位置上的entry,如果該entry不為空,則遍歷該entry所在的鏈表
  16. for (Entry<K, V> e = table[i]; e != null; e = e.next) {
  17. Object k;
  18. // 通過key的hashCode和equals方法判斷,key是否存在, 如果存在則用新的value取代舊的value,并返回舊的value
  19. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  20. V oldValue = e.value;
  21. e.value = value;
  22. e.recordAccess(this);
  23. return oldValue;
  24. }
  25. }
  26. // 修改次數(shù)增加1
  27. modCount++;
  28. // 如果找不到鏈表 或者 遍歷完鏈表后,發(fā)現(xiàn)key不存在,則創(chuàng)建一個新的Entry,并添加到HashMap中
  29. addEntry(hash, key, value, i);
  30. return null;
  31. }

put方法執(zhí)行流程:

  1. 檢查哈希表是否是個空表,如果是空表就調(diào)用inflateTable方法進(jìn)行初始化
  2. 判斷key是否為null,如果為null,就調(diào)用putForNullKey方法, 將key為null的key-value存儲在哈希表的第一個位置中
  3. 如果key不為null,則調(diào)用hash方法計(jì)算key的hash值
  4. 根據(jù)hash值和Entry數(shù)組的長度定位到Entry數(shù)組的指定槽位i
  5. 判斷Entry數(shù)組指定槽位的值e是否為null, 如果e不為null, 則遍歷e指向的單鏈表, 如果傳入的key在單鏈表中已經(jīng)存在了, 就進(jìn)行替換操作, 否則就新建一個Entry并添加到單鏈表的表頭位置
  6. 如果e為null, 就新建一個Entry并添加到指定槽位

       從put方法的執(zhí)行流程我們發(fā)現(xiàn), 在發(fā)生哈希碰撞的情況下, 插入key-value會遍歷指定槽位的單鏈表, 如果key已經(jīng)存在于單鏈表中了, 就會用value覆蓋舊的值, 如果key不存在, 則會將key-value插入單鏈表的表頭.  基于這個邏輯, put方法的算法復(fù)雜度就從O(1)變成了O(n), 因此優(yōu)化hash函數(shù), 減少哈希碰撞的發(fā)生, 就可以使得put方法的算法復(fù)雜度接近O(1).

4、get方法

  1. public V get(Object key) {
  2. // 如果key為null,調(diào)用getForNullKey方法從entry數(shù)組第一個位置獲取key對應(yīng)的value
  3. if (key == null) {
  4. return getForNullKey();
  5. }
  6. Entry<K, V> entry = getEntry(key);
  7. return null == entry ? null : entry.getValue();
  8. }
  1. final Entry<K, V> getEntry(Object key) {
  2. if (size == 0) {
  3. return null;
  4. }
  5. // 計(jì)算hash值
  6. int hash = (key == null) ? 0 : hash(key);
  7. // 通過hash值和數(shù)組長度計(jì)算出Entry數(shù)組的指定槽位
  8. for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
  9. Object k;
  10. // 通過hash值和equals判斷key是否存在,如果存在則返回對應(yīng)的value
  11. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { return e; }
  12. }
  13. return null;
  14. }

get方法執(zhí)行流程:

  1. 判斷key是否為null,如果為null,就調(diào)用getForNullKey方法, 從哈希表的第一個位置獲取
  2. 如果key不為null,調(diào)用hash方法計(jì)算key的Hash值
  3. 根據(jù)hash值和Entry數(shù)組的長度定位到Entry數(shù)組的指定槽位i
  4. 判斷Entry數(shù)組指定槽位的值e是否為null, 如果是則返回null
  5. 如果e不為null, 則遍歷e指向的單鏈表, 如果傳入的key在單鏈表中已經(jīng)存在了,

5、哈希表是如何初始化的?

  1. private void inflateTable(int toSize) {
  2. // 尋找大于toSize的,最小的,2的n次方作為新的容量
  3. int capacity = roundUpToPowerOf2(toSize);
  4. // 閥值=容量*負(fù)載因子, 如果容量*負(fù)載因子>最大容量時, 閥值=最大容量
  5. threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
  6. // 按新的容量創(chuàng)建一個新的數(shù)組
  7. table = new Entry[capacity];
  8. // 重新初始化hashSeed
  9. initHashSeedAsNeeded(capacity);
  10. }

6、HashMap是如何通過key的hash值來定位到Entry數(shù)組的指定槽位的?(size為什么必須是2的整數(shù)次冪?)

  1. static int indexFor(int h, int length) {
  2. // 對hash值和length-1進(jìn)行與運(yùn)算來計(jì)算索引
  3. return h & (length - 1);
  4. }

indexFor方法是根據(jù)hash值和entry數(shù)組的長度來計(jì)算出在數(shù)組中對應(yīng)的下標(biāo)。我們可以看到在這個方法內(nèi)部使用了與(&)操作符。與操作是對兩個操作數(shù)進(jìn)行位運(yùn)算,如果對應(yīng)的兩個位都為1,結(jié)果才為1,否則為0。與操作經(jīng)常用于去除操作數(shù)的高位值,例如:01011010 & 00001111 = 00001010。我們繼續(xù)回到代碼中,看看h&(length-1)做了些什么。

已知傳入的length是Entry數(shù)組的長度,我們知道數(shù)組下標(biāo)是從0開始計(jì)算的,所以數(shù)組的最大下標(biāo)為length-1。如果length為2的冪,那么length-1的二進(jìn)制位后面都為1。這時h&(length-1)的作用就是去掉了h的高位值,只留下h的低位值來作為數(shù)組的下標(biāo),h的低位值肯定不會比length-1大,所以可以保證數(shù)組不會越界。由此可以看到Entry數(shù)組的大小規(guī)定為2的冪就是為了能夠使用這個算法來確定數(shù)組的下標(biāo)。

 

7、哈希函數(shù)是怎樣計(jì)算Hash值的?

  1. final int hash(Object k) {
  2. int h = hashSeed;
  3. // 如果hashSeed不為0且key是字符串對象,則調(diào)用系統(tǒng)內(nèi)部提供的hash算法計(jì)算hash值
  4. if (0 != h && k instanceof String) {
  5. return sun.misc.Hashing.stringHash32((String) k);
  6. }
  7. h ^= k.hashCode();
  8. // 擾動函數(shù)
  9. h ^= (h >>> 20) ^ (h >>> 12);
  10. return h ^ (h >>> 7) ^ (h >>> 4);
  11. }

hash方法的最后兩行是真正計(jì)算hash值的算法,計(jì)算hash值的算法被稱為擾動函數(shù),所謂的擾動函數(shù)就是把所有東西雜糅到一起,可以看到這里使用了四個向右移位運(yùn)算。目的就是將h的高位值與低位值混合一下,以此增加低位值的隨機(jī)性。在上面我們知道定位數(shù)組的下標(biāo)是根據(jù)hash值的低位值來確定的。key的hash值是通過hashCode方法來生成的,而一個糟糕的hashCode方法生成的hash值的低位值可能會有很大的重復(fù)。為了使得hash值在數(shù)組上映射的比較均勻,擾動函數(shù)就派上用場了,把高位值的特性糅合進(jìn)低位值,增加低位值的隨機(jī)性,從而使散列分布的更加松散,以此提高性能。下圖舉了個例子幫助理解。

8、HashMap在什么時候判斷擴(kuò)容?是怎么進(jìn)行擴(kuò)容的?

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. // 如果鍵值對的總數(shù)大于等于閥值,且當(dāng)前要插入的key-value沒有發(fā)生hash碰撞,則進(jìn)行擴(kuò)容
  3. if ((size >= threshold) && (null != table[bucketIndex])) {
  4. // 擴(kuò)容到原來容量的兩倍
  5. resize(2 * table.length);
  6. // 擴(kuò)容后重新計(jì)算hash值
  7. hash = (null != key) ? hash(key) : 0;
  8. // 擴(kuò)容后重新確定Entry數(shù)組的槽位
  9. bucketIndex = indexFor(hash, table.length);
  10. }
  11. // 創(chuàng)建一個Entry對象,并添加到Entry數(shù)組的指定位置中
  12. createEntry(hash, key, value, bucketIndex);
  13. }
  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. // 如果舊數(shù)組的長度已經(jīng)達(dá)到最大值,則不進(jìn)行擴(kuò)容,并將閥值設(shè)置為最大值
  5. if (oldCapacity == MAXIMUM_CAPACITY) {
  6. threshold = Integer.MAX_VALUE;
  7. return;
  8. }
  9. // 以新的長度創(chuàng)建一個新的數(shù)組
  10. Entry[] newTable = new Entry[newCapacity];
  11. // initHashSeedAsNeeded(newCapacity) 確定是否重新進(jìn)行hash計(jì)算
  12. // 將舊數(shù)組中的元素逐個重新計(jì)算hash和index,然后全部轉(zhuǎn)移到新的數(shù)組中
  13. transfer(newTable, initHashSeedAsNeeded(newCapacity));
  14. table = newTable;
  15. // 將閥值設(shè)置為新的容量*負(fù)載率
  16. threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  17. }

     在調(diào)用put方法添加一個鍵值對時,如果集合中沒有存在的key就去調(diào)用addEntry方法新建一個Entry??吹缴厦尜N出的addEntry代碼,在新建一個Entry之前會先判斷當(dāng)前集合元素的大小是否超過了閥值,如果超過了閥值就調(diào)用resize進(jìn)行擴(kuò)容。傳入的新的容量是原來哈希表的兩倍,在resize方法內(nèi)部會新建一個容量為原先的2倍的Entry數(shù)組。然后將舊的哈希表里面的元素全部遷移到新的哈希表,其中可能會對舊的元素重新進(jìn)行哈希運(yùn)算,根據(jù)initHashSeedAsNeeded方法計(jì)算的值來確定是否重新計(jì)算哈希。完成哈希表的遷移之后,將當(dāng)前哈希表替換為新的,最后再根據(jù)新的哈希表容量來重新計(jì)算HashMap的閥值。由此可見,HashMap的擴(kuò)容操作時非常低效的,我們在創(chuàng)建HashMap對象時,可以先預(yù)估一下容量,然后指定一個初始容量,來減少擴(kuò)容的頻率,提高程序運(yùn)行的效率

9、替代哈希是怎么回事?

hash方法中首先會將hashSeed賦值給h。這個hashSeed就是哈希種子,它是一個隨機(jī)的值,作用就是幫助優(yōu)化哈希函數(shù)。hashSeed默認(rèn)是0,也就是默認(rèn)不使用替代哈希算法。那么什么時候使用hashSeed呢?首先需要設(shè)置開啟替代哈希,在系統(tǒng)屬性中設(shè)置jdk.map.althashing.threshold的值,在系統(tǒng)屬性中這個值默認(rèn)是-1,當(dāng)它是-1的時候使用替代哈希的閥值為Integer.MAX_VALUE。這也意味著可能你永遠(yuǎn)也不會使用替代哈希了。當(dāng)然你可以把這個閥值設(shè)小一點(diǎn),這樣當(dāng)集合元素達(dá)到閥值后就會生成一個隨機(jī)的hashSeed,以此增加hash函數(shù)的隨機(jī)性。為什么要使用替代哈希呢?當(dāng)集合元素達(dá)到你設(shè)定的閥值之后,意味著哈希表已經(jīng)比較飽和了,出現(xiàn)哈希沖突的可能性就會大大增加,這時對再添加進(jìn)來的元素使用更加隨機(jī)的散列函數(shù)能夠使后面添加進(jìn)來的元素更加隨機(jī)的分布在散列表中。

10、Fail-Fast機(jī)制:

   我們知道HashMap不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了map,那么將拋出ConcurrentModificationException,這就是所謂fail-fast策略。這一策略在源碼中的實(shí)現(xiàn)是通過modCount域,modCount顧名思義就是修改次數(shù),對HashMap內(nèi)容的修改都將增加這個值,那么在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount。

  1. private abstract class HashIterator<E> implements Iterator<E> {
  2. Entry<K,V> next; // next entry to return
  3. int expectedModCount; // For fast-fail
  4. int index; // current slot
  5. Entry<K,V> current; // current entry
  6. HashIterator() {
  7. expectedModCount = modCount;
  8. if (size > 0) { // advance to first entry
  9. Entry[] t = table;
  10. while (index < t.length && (next = t[index++]) == null)
  11. ;
  12. }
  13. }
  14. public final boolean hasNext() {
  15. return next != null;
  16. }
  17. final Entry<K,V> nextEntry() {
  18. if (modCount != expectedModCount)
  19. throw new ConcurrentModificationException();
  20. Entry<K,V> e = next;
  21. if (e == null)
  22. throw new NoSuchElementException();
  23. if ((next = e.next) == null) {
  24. Entry[] t = table;
  25. while (index < t.length && (next = t[index++]) == null)
  26. ;
  27. }
  28. current = e;
  29. return e;
  30. }
  31. public void remove() {
  32. if (current == null)
  33. throw new IllegalStateException();
  34. if (modCount != expectedModCount)
  35. throw new ConcurrentModificationException();
  36. Object k = current.key;
  37. current = null;
  38. HashMap.this.removeEntryForKey(k);
  39. expectedModCount = modCount;
  40. }
  41. }

modCount是volatile變量,保證了線程之間修改的可見性。在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經(jīng)有其他線程修改了Map。

在HashMap的API中指出:

由所有HashMap類所返回的迭代器都是快速失敗的:在迭代器創(chuàng)建之后,如果從結(jié)構(gòu)上對Map進(jìn)行修改,除了通過迭代器本身的 remove 方法之外,其他任何時間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對并發(fā)的修改,迭代器很快就會完全失敗,而不承擔(dān)在將來不確定的時間發(fā)生任意不確定行為的風(fēng)險(xiǎn)。

   注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的并發(fā)修改時,不可能作出任何堅(jiān)決的保證??焖偈〉鞅M最大努力拋出 ConcurrentModificationException。因此,編寫依賴于此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應(yīng)該僅用于檢測程序錯誤

11、HashMap在并發(fā)環(huán)境下有可能會出現(xiàn)死循環(huán)是怎么回事?

HashMap是線程不安全的,如果被多個線程共享的操作,將會引發(fā)不可預(yù)知的問題,據(jù)sun的說法,在擴(kuò)容時,會引起鏈表的閉環(huán),在get元素時,就會無限循環(huán),后果是cpu100%。

https:///articles/9606.html/comment-page-1

12、key為Null的鍵值對是如何存儲和查找的?

13、為什么HashMap常用String, Integer對象作為Key

14、如何正確使用HashMap?

a.不要在并發(fā)場景中使用HashMap,如果要使用,請換成CurrentHashMap

b.最好在初始化時,給HashMap設(shè)定一個合理的容量值

 

本文來自:我的博客,原文地址:https://blog.csdn.net/silentljh/article/details/80444216,轉(zhuǎn)載請注明。

 

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多