簡體   English   中英

Java 使用什么哈希函數來實現 Hashtable 類?

[英]What hashing function does Java use to implement Hashtable class?

從 CLRS(“算法導論”)一書中,有幾個散列函數,例如 mod、multiply 等。

Java 使用什么散列函數將鍵映射到槽?

我看到這里有一個問題Hashing function used in Java Language 但它沒有回答這個問題,我認為那個問題的標記答案是錯誤的。 它說 hashCode() 讓你為 Hashtable 做你自己的散列函數,但我認為這是錯誤的。

hashCode() 返回的整數是 Hashtble 的真正鍵,然后 Hashtable 使用散列函數對 hashCode() 進行散列。 這個答案意味着 Java 給了你一個機會給 Hashtable 一個散列函數,但不,這是錯誤的。 hashCode() 給出真正的密鑰,而不是散列函數。

那么Java到底使用了什么樣的哈希函數呢?

在 OpenJDK 中向 HashMap 添加或請求鍵時,執行流程如下:

  1. 使用開發人員定義的hashCode()方法將密鑰轉換為 32 位值。
  2. 然后,32 位值由第二個散列函數(安德魯的答案包含源代碼)轉換為散列表內的偏移量。 第二個散列函數由 HashMap 的實現提供,開發人員無法覆蓋。
  3. 如果哈希表中尚不存在鍵,則哈希表的相應條目包含對鏈表的引用或空值。 如果存在沖突(具有相同偏移量的幾個鍵),鍵和它們的值被簡單地收集在一個單向鏈表中。

如果哈希表的大小選擇得適當高,沖突的數量將受到限制。 因此,單次查找平均只需要恆定的時間。 這稱為預期常數時間 但是,如果攻擊者可以控制插入到哈希表中的密鑰並了解正在使用的哈希算法,他可能會引發大量哈希沖突,從而強制執行線性查找時間。 這就是為什么最近更改了一些哈希表實現以包含一個隨機元素,這使得攻擊者更難預測哪些鍵會導致沖突。

一些 ASCII 藝術

key.hashCode()
     |
     | 32-bit value
     |                              hash table
     V                            +------------+    +----------------------+
HashMap.hash() --+                | reference  | -> | key1 | value1 | null |
                 |                |------------|    +----------------------+
                 | modulo size    | null       |
                 | = offset       |------------|    +---------------------+
                 +--------------> | reference  | -> | key2 | value2 | ref |
                                  |------------|    +---------------------+
                                  |    ....    |                       |
                                                      +----------------+
                                                      V
                                                    +----------------------+
                                                    | key3 | value3 | null |
                                                    +----------------------+

根據hashmap 的來源(java 版本 < 8),每個 hashCode 都使用以下方法進行散列:

 /**
 * Applies a supplemental hash function to a given hashCode, which
 * defends against poor quality hash functions.  This is critical
 * because HashMap uses power-of-two length hash tables, that
 * otherwise encounter collisions for hashCodes that do not differ
 * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 */
static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

每個 hashCode 再次散列的原因是為了進一步防止沖突(見上面的評論)

HashMap 還使用一種方法來確定哈希碼索引(java 版本 < 8)(因為長度總是 2 的冪,您可以使用 & 代替 %):

/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length-1);
}

put 方法類似於:

int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);

哈希碼的目的是為給定對象提供唯一的整數表示。 因此,Integer 的 hashCode 方法只返回值是有道理的,因為每個值對於該 Integer 對象都是唯一的。

附加參考:
java8 的 HashMap
java11 的 HashMap

散列一般分為兩個步驟: a. 哈希碼 B. 壓縮

在步驟 a。 生成與您的密鑰相對應的整數。 這可以由您在 Java 中修改。

在步驟 b 中。 Java 應用了一種壓縮技術來映射步驟 a 返回的整數。 到哈希映射或哈希表中的一個插槽。 此壓縮技術無法更改。

/**
 * Computes key.hashCode() and spreads (XORs) higher bits of hash
 * to lower.  Because the table uses power-of-two masking, sets of
 * hashes that vary only in bits above the current mask will
 * always collide. (Among known examples are sets of Float keys
 * holding consecutive whole numbers in small tables.)  So we
 * apply a transform that spreads the impact of higher bits
 * downward. There is a tradeoff between speed, utility, and
 * quality of bit-spreading. Because many common sets of hashes
 * are already reasonably distributed (so don't benefit from
 * spreading), and because we use trees to handle large sets of
 * collisions in bins, we just XOR some shifted bits in the
 * cheapest possible way to reduce systematic lossage, as well as
 * to incorporate impact of the highest bits that would otherwise
 * never be used in index calculations because of table bounds.
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這是java中hashMap類使用的最新散列函數

我認為這里的概念有些混亂。 散列函數將可變大小的輸入映射到固定大小的輸出(散列值)。 對於 Java 對象,輸出是一個 32 位有符號整數。

Java 的 Hashtable 使用哈希值作為存儲實際對象的數組的索引,同時考慮了模運算和沖突。 然而,這不是散列。

java.util.HashMap 實現在索引之前對哈希值執行一些額外的位交換,以防止在某些情況下發生過度沖突。 它被稱為“附加哈希”,但我認為這不是一個正確的術語。

用一種非常簡單的方式來說,第二次散列就是找到存儲新鍵值對的桶數組的索引號。 完成此映射是為了從鍵 obj 的哈希碼的較大 int 值中獲取索引號。 現在,如果兩個不相等的鍵對象具有相同的哈希碼,則會發生沖突,因為它們將映射到相同的數組索引。 在這種情況下,第二個鍵及其值將被添加到鏈表中。 這里數組索引將指向添加的最后一個節點。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM