<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    JDK1.8中HashMap如何應對hash沖突?

    VSole2021-11-22 16:17:41

    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值。
    第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發生就講解完畢了!

    二進制hashmap
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    我們知道HashMap底層是由數組+鏈表/紅黑樹構成的,當我們通過put(key, value)向hashmap中添加元素時,需要通過散列函數確定元素究竟應該放置在數組中的哪個位置,當不同的元素被放置在了數據的同一個位置時,后放入的元素會以鏈表的形式,插在前一個元素的尾部,這個時候我們稱發生了hash沖突。
    序列化的核心思維旨在,將A變成B,最后再從B還原回A。 總之,在一些條件苛刻或者變化無常的環境與需求中,產生了這種靈活的可逆性的B的中間體。 理解不安全反序列化的最好方法是了解不同的編程語言如何實現序列化和反序列化。這里的序列化與反序列化指的是程序語言中自帶的實施與實現。而非自創或者自定義的序列化與反序列化機制(比如:N進制形式hashmap樹型等其他數據結構里的序列化中間體)。
    初衷是刷抖音太多,發現不能在點贊過的視頻列表中直接搜索,就想自己實現下,把這個過程做了下記錄,當學習筆記了,純技術交流用。
    它指的是一個有用的工具庫,幫助處理和操作XML格式的數據。ROME庫允許我們把XML數據轉換成Java中的對象,這樣我們可以更方便地在程序中操作數據。另外,它也支持將Java對象轉換成XML數據,這樣我們就可以把數據保存成XML文件或者發送給其他系統。
    如果指定了一個類為final,則該類所有的方法都是final的。此舉能夠使性能平均提高50% 。因為對這些大對象的操作會造成系統大的開銷,稍有不慎,將會導致嚴重的后果。
    Builtin實現了V8中大量的核心功能,可見它的重要性。
    上一篇文章介紹了xorstr的原理和最小化驗證概念的代碼,這篇文章來看下這種已經被廣泛應用于各惡意樣本以及安全組件中的技術如何還原,如果還沒看上篇建議先看下了解其實現后再看本篇文章。
    依賴于特定硬件環境的固件無法完整模擬,需要hook掉其中依賴于硬件的函數。LD_PRELOAD的劫持對于特定函數的劫持技術分為動態注入劫持和靜態注入劫持兩種。網上針對LD_PRELOAD的劫持也有大量的描述
    在學習漏洞的時候,按照0Day2書中第24章第1節的內容進行學習的,這章本來是遠程拒絕服務的漏洞(CVE-2009-3103),但是當我在網上搜索這個漏洞的EXP時,意外的發現了Srv2.sys模塊中的另一個漏洞(CVE-2009-2532),而這個漏洞竟然可以實現遠程任意代碼執行,誒,這我就不困了,然后順手兩個漏洞一起分析了,把Srv2.sys模塊對數據包的接收處理過程逆向了一遍,了解了其中的漏
    二進制代碼相似判斷有著廣泛的用途,如 Bug 搜索、惡意軟件聚類、惡意軟件檢測、惡意軟件譜系跟蹤、補丁生成、跨程序版本移植信息和軟件剽竊檢測等應用場景。其常見的八種應用如下所示:
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类