Java使用什么哈希函数来实现Hashtable类?

2022-08-31 15:19:45

从CLRS(“算法导论”)一书中,有几个哈希函数,如mod,乘法等。

Java使用什么哈希函数将密钥映射到插槽?

我已经看到这里有一个问题,即Java语言中使用的哈希函数。但它没有回答这个问题,我认为这个问题的标记答案是错误的。它说hashCode()可以让你为Hashtable做自己的哈希函数,但我认为这是错误的。

hashCode() 返回的整数是 Hashtble 的真正键,然后 Hashtable 使用哈希函数对 hashCode() 进行哈希处理。这个答案意味着Java给了你一个机会给Hashtable一个哈希函数,但是不,这是错误的。hashCode() 给出的是真正的密钥,而不是哈希函数。

那么Java到底使用什么哈希函数呢?


答案 1

当在 OpenJDK 中的 HashMap 中添加或请求密钥时,执行流程如下:

  1. 使用开发人员定义的方法将密钥转换为 32 位值。hashCode()
  2. 然后,32 位值由第二个哈希函数(Andrew 的答案包含源代码)转换为哈希表内的偏移量。第二个哈希函数由HashMap的实现提供,开发人员无法覆盖。
  3. 哈希表的相应条目包含对链接列表或 null 的引用(如果哈希表中尚不存在该键)。如果存在冲突(具有相同偏移量的多个键),则这些键及其值将简单地收集在单链表中。

如果哈希表大小选择的适当高,则冲突次数将受到限制。因此,单个查找平均只需要恒定的时间。这称为预期常量时间。但是,如果攻击者可以控制插入到哈希表中的密钥并了解正在使用的哈希算法,则他可以引发大量哈希冲突,从而强制线性查找时间。这就是为什么最近对一些哈希表实现进行了更改,以包含一个随机元素,这使得攻击者更难预测哪些密钥会导致冲突。

一些 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 |
                                                    +----------------------+

答案 2

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

每个哈希码再次被散列的原因是进一步防止冲突(见上面的评论)

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 对象都是唯一的。

附加参考:
HashMap for java8
HashMap for java11