繁体   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