對HashMap的一次記錄

余月七發表於2021-08-09

HashMap的具體學習,認識瞭解。


前言

也是最近開始面試才發現,HashMap是問的真多。以前聽學長或自己在網上看到過一些面試資料都在說集合、執行緒這塊比較重要,面試的重點。自己也是有那牴觸情緒,所以自認為這塊不重要,但最終發現自己真的太狹隘了,Map這塊的知識真的是對資料儲存有一個新的認識。但我現在認識尚淺,所以也真的說不出來什麼感悟。只能就是對這塊來一個簡單的入門吧(主要原因還是自己的不注重基礎知識的回顧,和一些重點原始碼的學習,導致時間一長,啥也不知道!)。
希望寫完這篇隨筆可以讓自己對這塊知識有個邏輯層次的認識。


一、回顧複習

1、Hash(雜湊)是什麼?

Hash,一般翻譯做雜湊、雜湊,或音譯為雜湊,是把任意長度的輸入(又叫做預對映pre-image)通過雜湊演算法變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不同的輸入可能會雜湊成相同的輸出,所以不可能從雜湊值來確定唯一的輸入值。簡單的說就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函式 —引自百度百科


2、Map是什麼?

首先將它意思翻譯過來是“地圖”“瞭解資訊”的意思;當然在Java中可以理解為“對映”;
Map它是Java提供的專門用來存放“鍵值對”這種形式物件的介面;可以把它認為也是一個集合框架。
下面這個圖真的見的很多很多,但卻是簡單明瞭的瞭解Java集合的好方法。
image


3、HashMap是什麼?

Tips:Map介面中的集合都有兩個泛型變數<K,V>,在使用時,要為兩個泛型變數賦予資料型別。兩個泛型變數<K,V>的資料型別可以相同,也可以不同

從維基百科中可以看到(我是直接用的谷歌翻譯)

In computing, a hash table (hash map) is a data structure that implements an associative array abstract data type, a structure that can map keys to values. A hash table uses a hash function to compute an index, also called a hash code, into an array of buckets or slots, from which the desired value can be found. During lookup, the key is hashed and the resulting hash indicates where the corresponding value is stored.
Ideally, the hash function will assign each key to a unique bucket, but most hash table designs employ an imperfect hash function, which might cause hash collisions where the hash function generates the same index for more than one key. Such collisions are typically accommodated in some way.
In a well-dimensioned hash table, the average cost (number of instructions) for each lookup is independent of the number of elements stored in the table. Many hash table designs also allow arbitrary insertions and deletions of key–value pairs, at (amortized[2]) constant average cost per operation.[3][4]
In many situations, hash tables turn out to be on average more efficient than search trees or any other table lookup structure. For this reason, they are widely used in many kinds of computer software, particularly for associative arrays, database indexing, caches, and sets.

計算中雜湊表雜湊對映)是一種實現關聯陣列抽象資料型別資料結構,該結構可以將對映到。雜湊表使用雜湊函式將_索引_(也稱為_雜湊碼_)計算到_桶_或_槽_陣列中,從中可以找到所需的值。在查詢過程中,鍵被雜湊,結果雜湊指示相應值的儲存位置。
理想情況下,雜湊函式會將每個鍵分配給一個唯一的桶,但大多數雜湊表設計採用不完善的雜湊函式,這可能會導致雜湊衝突,其中雜湊函式為多個鍵生成相同的索引。這種衝突通常以某種方式適應。
在維度良好的雜湊表中,每次查詢的平均成本(指令數)與儲存在表中的元素數量無關。許多雜湊表設計還允許任意插入和刪除鍵值對,每次操作的平均成本為(攤銷[2])不變。[3][4]
在許多情況下,雜湊表平均比搜尋樹或任何其他查詢結構更有效。為此,它們被廣泛用於多種計算機軟體,特別是關聯陣列資料庫索引快取集合


看原始碼的話我們可以知道,HashMap繼承了AbstractMap這個抽象類(此類提供Map介面的骨架實現,以最大限度地減少實現此介面所需的工作),然後AbstractMap實現了Map這個介面。

public class HashMap<K,V> extends AbstractMap<K,V> 
implements Map<K,V>, Cloneable, Serializable{……}


public abstract class AbstractMap<K,V> implements Map<K,V>{……}


public interface Map<K,V> {……}

到這就會發現其實collection中的set也很像HashMap。

4、熟悉的List、Set、Map區別問題

  • List(對付順序的好幫手): 儲存的元素是有序的、可重複的。
  • Set(注重獨一無二的性質): 儲存的元素是無序的、不可重複的。
  • Map(用 Key 來搜尋的專家): 使用鍵值對(key-value)儲存,類似於數學上的函式 y=f(x),“x”代表 key,"y"代表 value,Key 是無序的、不可重複的,value 是無序的、可重複的,每個鍵最多對映到一個值。

5、HashSet和HashMap的區別

如果你看過 HashSet 原始碼的話就應該知道:HashSet 底層就是基於 HashMap 實現的。(HashSet 的原始碼非常非常少,因為除了 clone()、writeObject()、readObject()是 HashSet 自己不得不實現之外,其他方法都是直接呼叫 HashMap 中的方法。 —引自JavaGuide作者文章內容

HashMap HashSet
實現了 Map 介面 實現 Set 介面
儲存鍵值對 僅儲存物件
呼叫 put()向 map 中新增元素 呼叫 add()方法向 Set 中新增元素
HashMap 使用鍵(Key)計算 hashcode HashSet 使用成員物件來計算 hashcode 值,對於兩個物件來說 hashcode 可能相同,所以equals()方法用來判斷物件的相等性

可以看一下具體繼承實現

public class HashSet<E> extends AbstractSet<E> 
implements Set<E>, Cloneable, java.io.Serializable{……}


public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {……}


public abstract class AbstractCollection<E> implements Collection<E> {……}
public interface Set<E> extends Collection<E> {……}


public interface Collection<E> extends Iterable<E> {……}
public interface Iterable<T> {……}


HashMap 提供了get方法,通過key值取對應的value值,
但是HashSet只能通過迭代器Iterator來遍歷資料,找物件。


二、面試問題

1、HashMap底層實現

JDK1.8 之前
HashMap 底層是 陣列和連結串列 結合在一起使用也就是 連結串列雜湊。HashMap 通過 key 的 hashCode 經過擾動函式處理過後得到 hash 值,然後通過 (n - 1) & hash 判斷當前元素存放的位置(這裡的 n 指的是陣列的長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鍊法解決衝突。
所謂擾動函式指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函式是為了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函式之後可以減少碰撞。

JDK1.8之後:
相比於之前的版本, JDK1.8 之後在解決雜湊衝突時有了較大的變化,當連結串列長度大於閾值(預設為 8)(將連結串列轉換成紅黑樹前會判斷,如果當前陣列的長度小於 64,那麼會選擇先進行陣列擴容,而不是轉換為紅黑樹)時,將連結串列轉化為紅黑樹,以減少搜尋時間

  • jdk8中新增了紅黑樹,當連結串列長度大於等於8的時候連結串列會變成紅黑樹

  • 連結串列新節點插入連結串列的順序不同(jdk7是插入頭結點,jdk8因為要把連結串列變為紅 黑樹所以採用插入尾節點)

  • hash演算法簡化 ( jdk8 )(jdk8中直接進行返回)

  • resize的邏輯修改(jdk7會出現死迴圈,jdk8不會)

  • Jdk1.7:陣列 + 連結串列 ( 當陣列下標相同,則會在該下標下使用連結串列)Jdk1.8:陣列 + 連結串列 + 紅黑樹 (預值為8 如果連結串列長度 >=8則會把連結串列變成紅黑樹 )

  • Jdk1.7中連結串列新元素新增到連結串列的頭結點,先加到連結串列的頭節點,再移到陣列下標位置

  • Jdk1.8中連結串列新元素新增到連結串列的尾結點(陣列通過下標索引查詢,所以查詢效率非常高,連結串列只能挨個遍歷,效率非常低。jdk1.8及以上版本引入了紅黑樹,當連結串列的長度大於或等於8的時候則會把連結串列變成紅黑樹,以提高查詢效率)

2、HashMap負載因子

負載因子:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     * 使用樹而不是列表的容器計數閾值。
     * 當向至少有這麼多節點的容器中新增元素時,
     * 容器被轉換為樹。該值必須大於2,並且應該至少為8,
     * 以與關於在收縮時轉換回普通bins的樹移除假設相匹配
     */

	/**
     *預設的負載因子是0.75f,也就是75% 負載因子的作用就是計算擴容閾值用,比如說使用
     *無參構造方法建立的HashMap 物件,他初始長度預設是16  閾值 = 當前長度 * 0.75  就
     *能算出閾值,噹噹前長度大於等於閾值的時候HashMap就會進行自動擴容
     */

loadFactor 載入因子是控制陣列存放資料的疏密程度,loadFactor 越趨近於 1,那麼 陣列中存放的資料(entry)也就越多,也就越密,也就是會讓連結串列的長度增加,loadFactor 越小,也就是趨近於 0,陣列中存放的資料(entry)也就越少,也就越稀疏。

loadFactor 太大導致查詢元素效率低,太小導致陣列的利用率低,存放的資料會很分散。loadFactor 的預設值為 0.75f 是官方給出的一個比較好的臨界值。(也是為了避免雜湊衝突)

給定的預設容量為 16,負載因子為 0.75。Map 在使用過程中不斷的往裡面存放資料,當數量達到了 16 * 0.75 = 12 就需要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複製資料等操作,所以非常消耗效能

3、HashMap預設長度為什麼是16

 /**
     * The default initial capacity - MUST be a power of two.
     * 預設的初始容量-必須是2的冪。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

解釋為什麼是2的冪次方:

為了能讓 HashMap 存取高效,儘量較少碰撞,也就是要儘量把資料分配均勻。我們上面也講到了過了,Hash 值的範圍值-2147483648 到 2147483647,前後加起來大概 40 億的對映空間,只要雜湊函式對映得比較均勻鬆散,一般應用是很難出現碰撞的。但問題是一個 40 億長度的陣列,記憶體是放不下的。所以這個雜湊值是不能直接拿來用的。用之前還要先做對陣列的長度取模運算,得到的餘數才能用來要存放的位置也就是對應的陣列下標。這個陣列下標的計算方法是“ (n - 1) & hash”。(n 代表陣列長度)。這也就解釋了 HashMap 的長度為什麼是 2 的冪次方。

這個演算法應該如何設計呢?
我們首先可能會想到採用%取餘的操作來實現。但是,重點來了:“取餘(%)操作中如果除數是 2 的冪次則等價於與其除數減一的與(&)操作(也就是說 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 並且 採用二進位制位操作 &,相對於%能夠提高運算效率,這就解釋了 HashMap 的長度為什麼是 2 的冪次方

解釋為什麼是16:

老問題又來了,為啥HashMap中初始化大小為什麼是16呢?​
首先我們看hashMap的原始碼可知當新put一個資料時會進行計算位於table陣列(也稱為桶)中的下標:

int index =key.hashCode()&(length-1);

hahmap每次擴容都是以 2的整數次冪進行擴容

因為是將二進位制進行按位於,(16-1) 是 1111,末位是1,這樣也能保證計算後的index既可以是奇數也可以是偶數,並且只要傳進來的key足夠分散,均勻那麼按位於的時候獲得的index就會減少重複,這樣也就減少了hash的碰撞以及hashMap的查詢效率。

那麼到了這裡你也許會問? 那麼就然16可以,是不是隻要是2的整數次冪就可以呢?

答案是肯定的。那為什麼不是8,4呢? 因為是8或者4的話很容易導致map擴容影響效能,如果分配的太大的話又會浪費資源,所以就使用16作為初始大小。

4、擴容機制

什麼場景下會觸發擴容?
場景1:雜湊table為null或長度為0;
場景2:Map中儲存的k-v對數量超過了閾值threshold;
場景3:連結串列中的長度超過了TREEIFY_THRESHOLD,但表長度卻小於MIN_TREEIFY_CAPACITY。

HashMap為了存取高效,要儘量較少碰撞,就是要儘量把資料分配均勻,每個連結串列長度大致相同,這個實現就在把資料存到哪個連結串列中的演算法;這個演算法實際就是取模,hash%length。
但是,大家都知道這種運算不如位移運算快。
因此,原始碼中做了優化hash&(length-1)。
也就是說hash%length==hash&(length-1)

那為什麼是2的n次方呢?
因為2的n次方實際就是1後面n個0,2的n次方-1,實際就是n個1。
例如長度為8時候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。
而長度為5的時候,3&(5-1)=0 2&(5-1)=0,都在0上,出現碰撞了。
所以,保證容積是2的n次方,是為了保證在做(length-1)的時候,每一位都能&1 ,也就是和1111……1111111進行與運算。

寫資料之後會可能觸發擴容,HashMap結構內,我記得有一個記錄當前資料量的欄位,這個資料量欄位到達擴容閾值的話,它就會觸發擴容的操作
閾值(threshold) = 負載因子(loadFactor) x 容量(capacity) 當HashMap中table陣列(也稱為桶)長度 >= 閾值(threshold) 就會自動進行擴容。

擴容的規則是這樣的,因為table陣列長度必須是2的次方數,擴容其實每次都是按照上一次tableSize位運算得到的就是做一次左移1位運算,
假設當前tableSize是16的話 16轉為二進位制再向左移一位就得到了32 即 16 << 1 == 32 即擴容後的容量,也就是說擴容後的容量是當前
容量的兩倍,但記住HashMap的擴容是採用當前容量向左位移一位(newtableSize = tableSize << 1),得到的擴容後容量,而不是當前容量x2


三、網上文章

以上部分基本是摘抄的,因為網上對這方面的解釋以及分析實在是多的很,想要自己真正的去理解,我感覺看原始碼是來的比較快的,畢竟原始碼中也有註釋,而且理解的話也不會有較大的偏差。上文一些參考文章如下:

JavaGuide面試題解
HashMap底層實現原理詳解
HashMap面試必問的6個點,你知道幾個?】
HashMap面試題,看這一篇就夠了!
Java集合面試題(總結最全面的面試題)

凡事都有利弊,我們能做的就是儘量趨利避害!


最後

別人的看法
  我也稱之為:活在他人的期待中。(比如,父母選的,不想讓父母失望)
我們終此一生,就是擺脫他人的期待,活出真正的自己!
你可以接受別人的觀點,但不是全盤接受,這樣你可以加以利用,把它變成屬於自己的產物,哪怕是我這篇令你顛覆自我的文章也是。
別人的觀點、看法,本質來說是自我的,是自己的三觀產物,因此不是完美的,從某種角度來說,甚至和你沒關係(比如我學計算機的,你學音樂的,你給我建議,說讓我學小提琴,完全沒道理啊!和我沒關係~)。
事實證明:人們之所以給你建議,是因為自己不安,這是他們給自己設定的目標,這不是你的目標,這是他們想要調節自己的認知
作者:Frank-FuckPPT https://www.bilibili.com/read/cv11220141 出處:bilibili

相關文章