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

67

在《算法导论》(CLRS)一书中,有几种哈希函数,例如mod、multiply等。

Java使用何种哈希函数将键映射到槽中呢?

我看到这里有一个问题:Hashing function used in Java Language。但它没有回答这个问题,而且我认为该问题的标记答案是错误的。它说hashCode()让你为Hashtable编写自己的哈希函数,但我认为这是错误的。

由hashCode()返回的整数才是Hashtable的真正键,然后Hashtable使用哈希函数来哈希hashCode()。这个答案暗示了Java给你一个机会为Hashtable提供一个哈希函数,但不,这是错误的。hashCode()给出了真正的键,而不是哈希函数。

那么Java究竟使用了哪个哈希函数呢?


http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7-b147/java/util/Hashtable.java#Hashtable - SLaks
@SLaks,那么"int index = (hash & 0x7FFFFFFF) % tab.length;"就是真正的哈希函数了吗? - Jackson Tale
可能是理解奇怪的Java哈希函数的重复问题。 - Raedwald
你可以在Stackoverflow上找到这个问题的答案:有用的链接 - Jeff
6个回答

111
当在OpenJDK中向HashMap添加或请求键时,执行流程如下:
  1. 使用开发人员定义的hashCode()方法将键转换为32位值。
  2. 然后,通过第二个哈希函数(源代码包含在Andrew的答案中)将32位值转换为哈希表内的偏移量。这个第二个哈希函数由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 |
                                                    +----------------------+

3
是的,这是关于哈希表的非常好的解释。非常清晰易懂。 - Jackson Tale
3
嗨Niklas,你是怎么画这个ASCII图的?它看起来非常棒! - janetsmith
1
@janetsmith:使用一个支持块编辑的好文本编辑器(VIM或EMACS,我记不清了)。 - Niklas B.
2
我以为你在使用这个http://www.jave.de/,它是一个不错的图像ASCII编辑器。 - janetsmith
1
@asdf 这里我指的是拒绝服务攻击,例如 https://www.ruby-lang.org/en/news/2011/12/28/denial-of-service-attack-was-found-for-rubys-hash-algorithm-cve-2011-4815/,https://medium.com/@bamieh/nodejs-constant-hashtables-seeds-vulnerability-f03bf70e3593 等。 - Niklas B.
显示剩余7条评论

35
根据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


15
我认为不是所有的hashCode都再次进行哈希只是为了进一步防止碰撞。我认为哈希函数试图将hashCode()值转换为底层数组的槽索引。 - Jackson Tale
1
顺便说一下,我认为这个函数在此期间已经被随机化了一些,以防止某些拒绝服务的情况,其中攻击者可以利用对哈希算法的知识来引发碰撞。 - Niklas B.
嗯,在OpenJDK方面,上述看起来似乎还不正确,算了。 - Niklas B.
1
快速问题,你能解释一下为什么人们选择这样的哈希函数吗?以及如何保证此函数的冲突较少。 - Albert Chen
1
@Jackson Tale hashCode() + hash() + indexFor() 一起尝试将一个对象映射到一个槽中。顺便说一句,看看HasnMap而不是HashTable。 - hotpro
有人知道这个哈希算法的名称和其属性吗?是否有64位版本? - QuantumKarl

4

总的来说,哈希分为两个步骤: a. 哈希码 b. 压缩

在a步骤中,会生成与您的键对应的整数。您可以在Java中对其进行修改。

在b步骤中,Java会应用一种压缩技术将由a步骤返回的整数映射到哈希表中的槽位。这种压缩技术无法更改。


3
/**
 * 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类所使用的最新哈希函数。


这是来自JDK8的代码。 - einverne

0

我认为这里有一些概念上的混淆。哈希函数将可变大小的输入映射到固定大小的输出(哈希值)。在Java对象的情况下,输出是一个32位有符号整数。

Java的Hashtable使用哈希值作为索引,进而找到实际存储对象的数组,考虑取模算术和冲突。然而,这不是哈希。

java.util.HashMap实现在索引之前对哈希值进行了一些额外的位交换,以防止某些情况下过多的冲突。它被称为“附加哈希”,但我认为这不是正确的术语。


1
“然而,这不是哈希。” 是的,它是。32位整数被用作另一个特定于该哈希表的哈希函数的输入(取决于向量的大小)。 - Niklas B.
@forty-two 你的意思是真正的哈希函数在hashCode()中,而决定数组槽索引的另一个函数不叫做哈希,只是索引的额外步骤? - Jackson Tale
@Jackson:请查看我的上面的评论。实际上,涉及到两个哈希函数,其中第二个将任意32位值映射到槽索引的确切范围内。 - Niklas B.
@NiklasB。是的,我看到了,我同意你的观点。但很多人只是认为hashCode()方法可以决定Hashtable的一切,这是正确的,我认为。 - Jackson Tale
@NiklasB。你能否写一个答案给这个问题,我会将你的标记为正确答案。 - Jackson Tale
@NiklasB。感谢您的回答。我实际上很喜欢它不是随机的,我的HashMap是确定性的。也许让开发人员选择是一个更好的主意。我很惊讶地发现,通过利用哈希碰撞,DoS攻击可以发生;这很有趣。 - Neo M Hacker

-1
以非常简单的方式来说,第二次哈希就是找到存储新键值对的桶数组的索引号。这个映射是通过从键对象的哈希码的较大整数值中获取索引号来完成的。如果两个不相等的键对象具有相同的哈希码,则会发生冲突,因为它们将被映射到同一个数组索引。在这种情况下,第二个键以及它的值将被添加到链表中。在这里,数组索引将指向最后添加的节点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接