日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

去哪面试都会问的HashMap

發布時間:2025/3/20 编程问答 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 去哪面试都会问的HashMap 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

??點擊上方?好好學java?,選擇?星標?公眾號

重磅資訊、干貨,第一時間送達 今日推薦:終于放棄了單調的swagger-ui了,選擇了這款神器—knife4j個人原創+1博客:點擊前往,查看更多

前言

HashMap可以說是面試的重中之重,去10家公司面試,8家都會問道,為什么大家都愛用HashMap打開話題?

HashMap是怎么實現的?

  • jdk1.7的HashMap是用數組+鏈表實現的

  • jdk1.8的HashMap是用數組+鏈表+紅黑樹實現的

  • 附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,目前,已經拿到了大廠offer,拿去不謝!

    下載方式

    1.?首先掃描下方二維碼

    2.?后臺回復「Java面試」即可獲取


    HashMap的主干是一個數組,假設我們有3個鍵值對dnf:1,cf:2,lol:3,每次放的時候會根據key.hash % table.length(對象的hashcode進行一些操作后對數組的長度取余)確定這個鍵值對應該放在數組的哪個位位置

    1 = indexFor(dnf),我們將鍵值對放在數組下標為1的位置


    3 = indexFor(cf) ?

    1 = indexFor(lol),這時發現數組下標為1的位置已經有值了,我們把lol:3放到鏈表的第一位,將原先的dnf:1用鏈表的形式放到lol鍵值對的下面

    jdk1.7是頭插法
    jdk1.8是尾插法


    在獲取key為dnf的鍵值對時,1=hash(dnf),得到這個鍵值對在數組下標為1的位置,dnf和lol不相等,和下一個元素比較,相等返回。set和get的過程就是這么簡單。先定位到槽的位置(即數組中的位置),再遍歷鏈表找到相同的元素。

    由上圖可以看出,HashMap在發生hash沖突的時候用的是鏈地址法,解決hash沖突并不只有這一種方法,常見的有如下四種方法

  • 開放定址法

  • 鏈地址法

  • 再哈希法

  • 公共溢出區域法。

  • JDK1.7源碼

    幾個重要的屬性

    //初始容量是16,且容量必須是2的倍數 static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<<?4;//最大容量是2的30次方 static?final?int?MAXIMUM_CAPACITY?=?1?<<?30;//負載因子 static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;static?final?Entry<?,?>[]?EMPTY_TABLE?=?{};//HashMap的主干是一個Entry數組,在需要的時候進行擴容,長度必須是2的被數 transient?Entry<K,V>[]?table?=?(Entry<K,V>[])?EMPTY_TABLE;//放置的key-value對的個數 transient?int?size;//進行擴容的閾值,值為?capacity?*?load?factor,即容量?*?負載因子 int?threshold;//負載因子 final?float?loadFactor;

    這里說一下threshold和loadFactor,threshold = capacity * load factor,即擴容的閾值=數組長度 * 負載因子,如果hashmap中放了16個元素,負載因子為0.75,則擴容閾值為16*0.75=12

  • 負載因子越小,容易擴容,浪費空間,但查找效率高

  • 負載因子越大,不易擴容,對空間的利用更加充分,查找效率低(鏈表拉長)

  • 存儲數據的靜態內部類,數組+鏈表,這里的數組指的就是Entry數組

    static?class?Entry<K,V>?implements?Map.Entry<K,V>?{final?K?key;V?value;Entry<K,V>?next;//存儲指向下一個Entry的引用,單鏈表結構int?hash;//對key的hashcode值進行hash運算后得到的值,存儲在Entry,避免重復計算Entry(int?h,?K?k,?V?v,?Entry<K,V>?n)?{value?=?v;next?=?n;key?=?k;hash?=?h;} }

    構造函數

    其他都是在此基礎上的擴展,主要就是設置初始容量和負載因子,這2個參數前面介紹過了哈。

    public?HashMap(int?initialCapacity,?float?loadFactor)?{if?(initialCapacity?<?0)throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+initialCapacity);if?(initialCapacity?>?MAXIMUM_CAPACITY)initialCapacity?=?MAXIMUM_CAPACITY;if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))throw?new?IllegalArgumentException("Illegal?load?factor:?"?+loadFactor);this.loadFactor?=?loadFactor;threshold?=?initialCapacity;init(); }

    最重要的知識點來了,對著流程看源碼比較好理解

    put方法的執行過程

  • key為null直接放在table[0]處,對key的hashCode()做hash運算,計算index;

  • 如果節點已經存在就替換old value(保證key的唯一性),并返回old Value

  • 如果達到擴容的閾值(超過capacity * load factor),并且發生碰撞,就要resize

  • 將元素放到bucket的首位,即頭插法

  • public?V?put(K?key,?V?value)?{//hashmap的數組為空if?(table?==?EMPTY_TABLE)?{inflateTable(threshold);}if?(key?==?null)return?putForNullKey(value);//獲取hash值int?hash?=?hash(key);//找到應該放到table的哪個位置int?i?=?indexFor(hash,?table.length);//遍歷table[i]位置的鏈表,查找相同的key,若找到則使用新的value替換oldValue,并返回oldValuefor?(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++;//將元素放到table[i],新的元素總在table[i]位置的第一個元素,原來的元素后移addEntry(hash,?key,?value,?i);return?null; }

    為空時,HashMap還沒有創建這個數組,有可能用的是默認的16的初始值,還有可能自定義了長度,這時需要把數組長度變為2的最小倍數,并且這個2的倍數大于等于初始容量

    private?void?inflateTable(int?toSize)?{//返回大于或等于最接近2的冪數int?capacity?=?roundUpToPowerOf2(toSize);threshold?=?(int)?Math.min(capacity?*?loadFactor,?MAXIMUM_CAPACITY?+?1);table?=?new?Entry[capacity];initHashSeedAsNeeded(capacity); }

    若key為null,則將值放在table[0]這個鏈上

    private?V?putForNullKey(V?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++;addEntry(0,?null,?value,?0);return?null; }

    找到應該放在數組的位置,h & (length-1)這個式子你可以認為hash值對數組長度取余,后面會說到這個式子

    static?int?indexFor(int?h,?int?length)?{//?assert?Integer.bitCount(length)?==?1?:?"length?must?be?a?non-zero?power?of?2";return?h?&?(length-1); }

    添加元素

    void?addEntry(int?hash,?K?key,?V?value,?int?bucketIndex)?{//?容量超過閾值,并且發生碰撞時進行擴容if?((size?>=?threshold)?&&?(null?!=?table[bucketIndex]))?{//?數組擴容為原來的2倍,并將元素復制到新數組上resize(2?*?table.length);//?重新計算hash值,如果不做特殊設置的話,和之前算出來的hash值一樣hash?=?(null?!=?key)???hash(key)?:?0;bucketIndex?=?indexFor(hash,?table.length);}createEntry(hash,?key,?value,?bucketIndex); }

    將新增加的元素放到table的第一位,并且將其他元素跟在第一個元素后面

    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++; }

    容量超過閾值并且發生碰撞,開始擴容

    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); }

    重新計算元素在新的數組中的位置,并進行復制處理,initHashSeedAsNeeded函數默認情況下會一直返回false,即rehash在默認情況下為false

    void?transfer(Entry[]?newTable,?boolean?rehash)?{int?newCapacity?=?newTable.length;//?遍歷數組for?(Entry<K,V>?e?:?table)?{//?遍歷鏈表while(null?!=?e)?{Entry<K,V>?next?=?e.next;if?(rehash)?{e.hash?=?null?==?e.key???0?:?hash(e.key);}int?i?=?indexFor(e.hash,?newCapacity);e.next?=?newTable[i];newTable[i]?=?e;e?=?next;}} }

    這個transfer函數挺有意思的,如果你仔細理解它的復制過程,會發現有如下2個特別有意思的地方

  • 原來在oldTable[i]位置的元素,會被放到newTable[i]或者newTable[i+oldTable.length]的位置

  • 鏈表在轉移的時候會反轉

  • 這2個點需要注意一下,我會在JDK1.8中再次提到這2個點

    get方法的執行過程

  • key為null直接從table[0]處取,對key的hashCode()做hash運算,計算index;

  • 通過key.equals(k)去查找對應的Entry,接著返回value

  • public?V?get(Object?key)?{if?(key?==?null)return?getForNullKey();Entry<K,V>?entry?=?getEntry(key);return?null?==?entry???null?:?entry.getValue(); }

    從table[0]初獲取key為null的值

    private?V?getForNullKey()?{if?(size?==?0)?{return?null;}for?(Entry<K,V>?e?=?table[0];?e?!=?null;?e?=?e.next)?{if?(e.key?==?null)return?e.value;}return?null; }

    key不為null時

    final?Entry<K,V>?getEntry(Object?key)?{if?(size?==?0)?{return?null;}int?hash?=?(key?==?null)???0?:?hash(key);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; }

    JDK1.8源碼

    jdk1.8存取key為null的數據并沒有進行特判,而是通過將hash值返回為0將其放在table[0]處

    put執行過程

  • 對key的hashcode()高16位和低16位進行異或運算求出具體的hash值

  • 如果table數組沒有初始化,則初始化table數組長度為16

  • 根據hash值計算index,如果沒碰撞則直接放到bucket里(bucket可為鏈表或者紅黑樹)

  • 如果碰撞導致鏈表過長,就把鏈表轉為紅黑樹

  • 如果key已經存在,用new value替換old value,并返回old value

  • 如果超過擴容的閾值則進行擴容,threshold = capacity * load factor

  • jdk1.8和jdk1.7重新獲取元素值在新數組中所處的位置的算法發生了變化(實際效果沒發生改變)

  • jdk1.7,hash & (length-1)

  • jdk1.8,判斷原來hash值要新增的bit位是0還是1。假如是0,放到newTable[i],否則放到newTable[i+oldTable.length]

  • get執行過程

  • 對key的hashcode()高16位和低16位進行異或運算求出具體的hash值

  • 如果在bucket里的第一個節點直接命中,則直接返回

  • 如果有沖突,通過key.equals(k)去查找對應的Node,并返回value。在樹中查找的效率為O(logn),在鏈表中查找的效率為O(n)

  • 常見面試題

    HashMap,HashTable,ConcurrentHashMap之間的區別

    對象key和value是否允許為空是否線程安全
    HashMapkey和value都允許為null
    HashTablekey和value都不允許為null
    ConcurrentHashMapkey和value都不允許為null

    HashMap在什么條件下擴容

    jdk1.7

  • 超過擴容的閾值

  • 發生碰撞

  • jdk1.8

  • 超過擴容的閾值

  • HashMap的大小為什么是2的n次方

    為了通過hash值確定元素在數組中存的位置,我們需要進行如下操作hash%length,當時%操作比較耗時間,所以優化為 hash & (length - 1)

    當length為2的n次方時,hash & (length - 1) =hash % length

    我們假設數組的長度為15和16,hash碼為8和9

    h & (length - 1)hlengthindex
    8 & (15 - 1)010011100100
    9 & (15 - 1)010111100100
    8 & (16 - 1)010011110100
    9 & (16 - 1)010111110101

    可以看出數組長度為15的時候,hash碼為8和9的元素被放到數組中的同一個位置形成鏈表,鍵低了查詢效率,當hahs碼和15-1(1110)進行&時,最后一位永遠是0,這樣0001,0011,0101,1001,1011,0111,1101這些位置永遠不會被放置元素,這樣會導致

  • 空間浪費大

  • 增加了碰撞的幾率,減慢查詢的效率

  • 當數組長度為時,的所有位都是1,如8-1=7即111,那么進行低位&運算時,值總與原來的hash值相同,降低了碰撞的概率

    JDK1.8發生了哪些變化?

  • 由數組+鏈表改為數組+鏈表+紅黑樹,當鏈表的長度超過8時,鏈表變為紅黑樹。
    為什么要這么改?
    我們知道鏈表的查找效率為O(n),而紅黑樹的查找效率為O(logn),查找效率變高了。
    為什么不直接用紅黑樹?
    因為紅黑樹的查找效率雖然變高了,但是插入效率變低了,如果從一開始就用紅黑樹并不合適。從概率學的角度選了一個合適的臨界值為8

  • 優化了hash算法

  • 計算元素在新數組中位置的算法發生了變化,新算法通過新增位判斷oldTable[i]應該放在newTable[i]還是newTable[i+oldTable.length]

  • 頭插法改為尾插法,擴容時鏈表沒有發生倒置(避免形成死循環)

  • HashMap在高并發下會發生什么問題?

  • 多線程擴容,會讓鏈表形成環,從而造成死循環

  • 多線程put可能導致元素丟失

  • 如何避免HashMap在高并發下的問題?

  • 使用ConcurrentHashMap

  • 用Collections.synchronizedMap(hashMap)包裝成同步集合

  • 最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,目前,已經拿到了大廠offer,拿去不謝!

    下載方式

    1.?首先掃描下方二維碼

    2.?后臺回復「Java面試」即可獲取

    總結

    以上是生活随笔為你收集整理的去哪面试都会问的HashMap的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。