Java 使用啥散列函数来实现 Hashtable 类?

Posted

技术标签:

【中文标题】Java 使用啥散列函数来实现 Hashtable 类?【英文标题】:What hashing function does Java use to implement Hashtable class?Java 使用什么散列函数来实现 Hashtable 类? 【发布时间】:2012-03-10 23:18:11 【问题描述】:

从书CLRS(《算法导论》)中,有几种散列函数,如mod、multiply等。

Java 使用什么散列函数将键映射到槽?

我看到这里有一个问题Hashing function used in Java Language。但它没有回答问题,我认为该问题的标记答案是错误的。它说 hashCode() 可以让你为 Hashtable 做自己的散列函数,但我认为这是错误的。

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

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

【问题讨论】:

grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… @SLaks, 所以“int index = (hash & 0x7FFFFFFF) % tab.length;”是真正的散列函数吗? Understanding strange Java hash function的可能重复 您可能会发现这个 *** 问题答案:[帮助链接][1] [1]:***.com/questions/13825546/a-faster-hash-function 【参考方案1】:

在 OpenJDK 中,当向 HashMap 添加或请求键时,执行流程如下:

    使用开发人员定义的 hashCode() 方法将密钥转换为 32 位值。 32 位值随后由第二个散列函数(其中 Andrew 的答案包含源代码)转换为散列表内的偏移量。第二个哈希函数由 HashMap 的实现提供,开发人员无法覆盖。 如果哈希表中尚不存在键,则哈希表的相应条目包含对链表的引用或 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 |
                                                    +----------------------+

【讨论】:

是的,这是对Hashtable的一个很好的解释。很清楚。 嗨 Niklas,你是如何绘制这个 ASCII 图的?真的很好看! @janetsmith:使用支持基于块的编辑的优秀文本编辑器(我不记得是 VIM 或 EMACS)。 我以为你在用这个 jave.de ,它是一个不错的图像 ascii 编辑器 @asdf 这里我指的是拒绝服务攻击,例如ruby-lang.org/en/news/2011/12/28/…、medium.com/@bamieh/… 等【参考方案2】:

根据hashmap's source(java version

 /**
 * 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 进行哈希处理的原因是为了进一步防止冲突(参见上面的 cmets)

HashMap 还使用了一种方法来确定index of a hash code(java version

/**
 * 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 java8HashMap for java11

【讨论】:

我不认为每个 hashCode 都被再次散列只是为了进一步防止冲突。我认为散列函数是尝试将 hashCode() 值转换为底层数组的槽索引。 顺便说一下,我认为这个函数在此期间已经被随机化了一点,以防止某些拒绝服务场景,攻击者可以利用哈希算法的知识来引发冲突。 嗯,上面的内容对于 OpenJDK 来说似乎还不是真的,没关系。 快速提问,可以解释为什么人们会选择这样的哈希函数,以及如何保证这个函数的冲突很少。 @Jackson Tale hashCode() + hash() + indexFor() 一起尝试将一个对象映射到一个槽中。顺便说一句,看看 HasnMap 而不是 HashTable。【参考方案3】:

散列一般分为两个步骤: 一种。哈希码 湾。压缩

在步骤 a。生成一个与您的密钥相对应的整数。这可以由您在 Java 中进行修改。

在步骤 b。 Java 应用了一种压缩技术来映射步骤 a 返回的整数。到 hashmap 或 hashtable 中的一个槽。此压缩技术无法更改。

【讨论】:

【参考方案4】:
/**
 * 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类使用的最新hash函数

【讨论】:

【参考方案5】:

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

Java 的 Hashtable 使用哈希值作为存储实际对象的数组的索引,同时考虑了模运算和冲突。但是,这不是散列。

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

【讨论】:

“但是,这不是散列。”是的。 32 位整数用作另一个特定于该哈希表的哈希函数的输入(取决于向量的大小)。 @forty-2 所以你的意思是真正的散列函数在 hashCode() 中,而另一个决定数组槽索引的函数不称为散列,而只是索引的一个额外步骤? @Jackson:见我上面的评论。实际上涉及两个哈希函数,其中第二个将任意 32 位值映射到插槽索引的确切范围。 @NiklasB.是的,我看到了,我同意你的看法。但是很多人只是认为 hashCode() 可以决定 Hashtable 的一切,我认为这是真的 @NiklasB。你能写出这个问题的答案吗,我会把你的答案标记为正确的【参考方案6】:

简单地说,第二次散列就是找到存储新键值对的桶数组的索引号。完成此映射是为了从键 obj 的哈希码的较大 int 值中获取索引号。现在,如果两个不相等的键对象具有相同的哈希码,则会发生冲突,因为它们将映射到相同的数组索引。在这种情况下,第二个键及其值将被添加到链表中。这里的数组索引将指向最后添加的节点。

【讨论】:

以上是关于Java 使用啥散列函数来实现 Hashtable 类?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 散列表(HashTable)

HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别

初识java集合——散列表(HashTable)

算法---hash算法原理(java中HashMap底层实现原理和源码解析)

HashTable 及应用

java读书笔记---HashMap和HashTable