美團一面:JDK 1.8 中的 HashMap 如何應對 hash 衝突?我懵逼了。。

来源:https://www.cnblogs.com/javastack/archive/2022/06/14/16374771.html
-Advertisement-
Play Games

1 什麼是hash衝突 我們知道HashMap底層是由數組+鏈表/紅黑樹構成的,當我們通過put(key, value)向hashmap中添加元素時,需要通過散列函數確定元素究竟應該放置在數組中的哪個位置,當不同的元素被放置在了數據的同一個位置時,後放入的元素會以鏈表的形式,插在前一個元素的尾部,這 ...


1 什麼是hash衝突

我們知道HashMap底層是由數組+鏈表/紅黑樹構成的,當我們通過put(key, value)向hashmap中添加元素時,需要通過散列函數確定元素究竟應該放置在數組中的哪個位置,當不同的元素被放置在了數據的同一個位置時,後放入的元素會以鏈表的形式,插在前一個元素的尾部,這個時候我們稱發生了hash衝突。

2 如何解決hash衝突

事實上,想讓hash衝突完全不發生,是不太可能的,我們能做的只是儘可能的降低hash衝突發生的概率:下麵介紹在HashMap中是如何應對hash衝突的?

當我們向hashmap中put元素(key, value)時,最終會執行putVal()方法,而在putVal()方法中,又執行了hash(key)這個操作,並將執行結果作為參數傳遞給了putVal方法。那麼我們先來看hash(key)方法幹了什麼。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
   // 判斷key是否為null, 如果為null,則直接返回0;
   // 如果不為null,則返回(h = key.hashCode()) ^ (h >>> 16)的執行結果
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(h = key.hashCode()) ^ (h >>> 16)執行了三步操作 :我們一步一步來分析:

第1步:h = key.hashCode()

這一步會根據key值計算出一個int類型的h值也就是hashcode值,例如

"helloWorld".hashCode() --> -1554135584
"123456".hashCode() --> 1450575459
"我愛java".hashCode() --> -1588929438

至於hashCode()是如何根據key計算出hashcode值的,要分幾種情況進行分析:

  1. 如果我們使用的自己創建的對象,在我們沒有重寫hashCode()方法的情況下,會調用Object類的hashCode()方法,而此時返回就是對象的記憶體地址值,所以如果對象不同,那麼通過hashcode()計算出的hashcode就是不同的。
  2. 如果是使用java中定義的引用類型例如String,Integer等作為key,這些類一般都會重寫hashCode()方法,有興趣可以翻看一下對應的源碼。簡單來說,Integer類的hashCode()返回的就是Integer值,而String類型的hashCode()方法稍稍複雜一點,這裡不做展開。總的來說,hashCode()方法的作用就是要根據不同的key得到不同的hashCode值。

JDK 8 系列教程:

https://www.javastack.cn/categories/Java/Java8/

第2步:h >>> 16

這一步將第1步計算出的h值無符號右移16位。

為什麼要右移16位,當然是位了第三步的操作。

第3步:h ^ (h >>> 16)

將hashcode值的高低16位進行異或操作(同0得0、同1得0、不同得1)得到hash值,舉例說明:

  • 假設h值為:1290846991
  • 它的二進位數為:01001100 11110000 11000011 00001111
  • 右移十六位之後:00000000 00000000 01001100 11110000
  • 進行異或操作後:01001100 11110000 10001100 11110000
  • 最終得到的hash值:1290833136

那麼問題來了: 明明通過第一步得到的hashcode值就可以作為hash返回,為什麼還要要進行第二步和第三步的操作呢?答案是為了減少hash衝突!

元素在數組中存放的位置是由下麵這行代碼決定的:

// 將(數組的長度-1)和hash值進行按位與操作:
i = (n - 1) & hash  // i為數組對應位置的索引  n為當前數組的大小

我們將上面這步操作作為第4步操作,來對比一下執行1、2、3、4四個步驟和只執行第1、4兩個步驟所產生的不同效果。

我們向hashmap中put兩個元素node1(key1, value1)node2(key2, value2),hashmap的數組長度n=16

執行1、2、3、4 四個步驟:

1.h = key.hashCode()

  • 假設計算的結果為:h = 3654061296
  • 對應的二進位數為:    01101100 11100110 10001100 11110000

2.h >>> 16

  • h無符號右移16位得到:00000000 00000000 01101100 11100110

3.hash = h ^ (h >>> 16)

  • 異或操作後得到hash:  01101100 11110000 11100000 00000110

4.i = (n-1) & hash

  • n-1=15 對應二進位數 :    00000000 00000000 00000000 00001111
  • hash :   01101100 11110000 11100000 00000110
  • hash & 15 :    00000000 00000000 00000000 00000110
  • 轉化為10進位 :&ensp 5

最終得到i的值為5,也就是說node1存放在數組索引為5的位置。

同理我們對(key2, value2) 進行上述同樣的操作過程:

1.h = key.hashCode()

  • 假設計算的結果為:h = 3652881648
  • 對應的二進位數為:    01101100 11011101 10001100 11110000

2.h >>> 16

  • h無符號右移16位得到:00000000 00000000 01101100 11011101

3.hash = h ^ (h >>> 16)

  • 異或操作後得到hash:  01101100 11110000 11100000 00101101

4.i = (n-1) & hash

  • n-1=15 對應二進位數 :    00000000 00000000 00000000 00001111
  • hash :   01101100 11110000 11100000 00101101
  • hash & 15 :   00000000 00000000 00000000 00001101
  • 轉化為10進位 :&ensp 13

最終得到i的值為13,也就是說node2存放在數組索引為13的位置

node1和node2存儲的位置如下圖所示:

執行1、4兩個步驟:

1.h = key.hashCode()

  • 計算的結果同樣為:h = 3654061296
  • 對應的二進位數為:    01101100 11100110 10001100 11110000

4.i = (n-1) & hash

  • n-1=15 對應二進位數 :    00000000 00000000 00000000 00001111
  • hash(h) :   01101100 11100110 10001100 11110000
  • hash & 15 :   00000000 00000000 00000000 00000000
  • 轉化為10進位 :  0

最終得到i的值為0,也就是說node1存放在數組索引為0的位置

同理我們對(key2, value2) 進行上述同樣的操作過程:

1.h = key.hashCode()

  • 計算的結果同樣為:h = 3652881648
  • 對應的二進位數為:    01101100 11011101 10001100 11110000

4.i = (n-1) & hash

  • n-1=15 對應二進位數 :    00000000 00000000 00000000 00001111
  • hash(h) :   01101100 11110000 11100000 11110000
  • hash & 15 :   00000000 00000000 00000000 00000000
  • 轉化為10進位 :  0

最終得到i的值為0,也就是說node2同樣存放在數組索引為0的位置

node1和node2存儲的位置如下圖所示:

相信大家已經看出區別了:

當數組長度n較小時,n-1的二進位數高16位全部位0,這個時候如果直接和h值進行&(按位與)操作,那麼只能利用到h值的低16位數據,這個時候會大大增加hash衝突發生的可能性,因為不同的h值轉化為2進位後低16位是有可能相同的,如上面所舉例子中:key1.hashCode()key2.hashCode() 得到的h值不同,一個h1 = 3654061296 ,另一個h2 = 3652881648,但是不幸的是這h1、h2兩個數轉化為2進位後低16位是完全相同的,所以h1 & (n-1)h2 & (n-1) 會計算出相同的結果,這也導致了node1和node2 存儲在了數組索引相同的位置,發生了hash衝突。

當我們使用進行 h ^ (h >>> 16) 操作時,會將h的高16位數據和低16位數據進行異或操作,最終得出的hash值的高16位保留了h值的高16位數據,而hash值的低16數據則是h值的高低16位數據共同作用的結果。所以即使h1和h2的低16位相同,最終計算出的hash值低16位也大概率是不同的,降低了hash衝突發生的概率。

ps:這裡面還有一個值的註意的點: 為什麼是(n-1)?

我們知道n是hashmap中數組的長度,那麼為要進行n-1的操作?答案同樣是為了降低hash衝突發生的概率!

要理解這一點,我們首先要知道HashMap規定了數組的長度n必須為2的整數次冪,至於為什麼是2的整數次冪,會在HashMap的擴容方法resize()里詳細講。

既然n為2的整數次冪,那麼n一定是一個偶數。那麼我們來比較i = hash & ni = hash & (n-1)有什麼異同。

n為偶數,那麼n轉化為2進位後最低位一定為0,與hash進行按位與操作後最低位仍一定為0,這就導致i值只能為偶數,這樣就浪費了數組中索引為奇數的空間,同時也增加了hash衝突發生的概率。

所以我們要執行n-1,得到一個奇數,這樣n-1轉化為二進位後低位一定為1,與hash進行按位與操作後最低位即可能位0也可能位1,這就是使得i值即可能為偶數,也可能為奇數,充分利用了數組的空間,降低hash衝突發生的概率。

至此, JDK1.8中 HashMap 是如何在存儲元素時減少hash發生就講解完畢了!

來源:blog.csdn.net/weixin_43689776/article/details/99999126

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 0. 前情提要 面試官: 你能手寫個LRU緩存嗎? 你: LRU是什麼東西?(一臉懵逼狀) 面試官: LRU全稱Least Recently Used(最近最少使用),用來淘汰不常用數據,保留熱點數據。 你寫了5分鐘,然而只寫了個get和put方法體,裡面邏輯實在不知道咋寫。 面試官: 今天的面試先 ...
  • 順序執行 C 語言的程式是順序執行,即先執行前面的語句,再執行後面的語句。 條件執行 if if語句用於條件判斷,滿足條件時,就執行指定的語句。 if (expression) { statement // 表達式expression為真(非 0 值)時,就執行 statement 語句。 } if ...
  • @ 一.前言 使用PyQt5模仿網易雲音樂,只有UI沒有功能。 二.展示-主界面 1.靜圖1 主界面 2.靜圖2 主界面-歌單 3.靜圖3 主界面-播客 3.靜圖3 主界面-最新音樂 4.動圖1 主界面-綜合動圖展示 三.展示-登錄界面 仿照製作了一個登錄頁面(二維碼是可以掃描的,並且具有時效性哦~ ...
  • C++預設參數及其本質 1. 概述 概述 C++ 允許函數設置預設參數,在調用時可以根據情況省略實參 其規則如下 預設參數只能按照從右到左的順序。 如果函數同時有聲明、實現、預設參數只能放在函數聲明中 預設參數的值可以是常量、全局符號(全局變數、函數名) 實例1:預設參數只能按照從右到左的順序、預設 ...
  • blind watermark 盲水印技術實現,這裡主要引用網上三種java實現的效果測試和研究。可以將文字隱藏在圖片中,通過提取還原水印,實現版本保護效果。 開源代碼: https://gitee.com/chejiangyi/shuiyin/tree/master ...
  • Practical usage of cpp reference and move semantic 在優化重構一部分老代碼時,實際使用 c++ 的 reference 與 move semantic 遇到了若幹問題,在此記錄。 Aggregation 首先,數據的設計並不複雜,只有一個類,成員變數 ...
  • C++函數重載的原理 一、函數重載概述 1.1 為什麼要有函數重載 在實際的開發中,有時候我們需要實現幾個功能類似的函數,只是有些細節不同。例如希望交換兩個變數的值,但是這兩個變數可能有多種類型:int、char、double、bool等。在C語言中,程式員往往需要分別設計出多個不同名的函數,但是在 ...
  • 前言 哈嘍,大家好。有沒有誰跟我一樣喜歡老照片的朋友,老照片總是讓人感覺有一種情懷,可能這就是懷念吧。有一次無意中看到 了很久很久以前的照片都是黑白的,當時很想給照片添加點顏色,但是不知道怎麼搞。今天,我終於發現了怎麼去解決這個問 題,想了想,我決定得把這個分享給大家… 今天我們分享用NoGAN的圖 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...