Java hashCode、实现以及与HashMap的关系

Java hashCode、实现以及与HashMap的关系,java,hashmap,hashcode,Java,Hashmap,Hashcode,所以我问了另一个相关的问题:,但我现在有一个不同的相关问题 我在那个问题中确定的是,String的hashCode()函数没有雪崩效应。这意味着,例如,如果我有字符串“k1”、“k2”、“k3”,并且我对每个字符串调用hashCode(),则返回的值将是连续的 现在,根据我对数据结构101的记忆,我觉得这是件坏事。因为假设HashMap通过如下算法选择存储桶: class HashMap { private int capacity; private int chooseBuck

所以我问了另一个相关的问题:,但我现在有一个不同的相关问题

我在那个问题中确定的是,String的hashCode()函数没有雪崩效应。这意味着,例如,如果我有字符串“k1”、“k2”、“k3”,并且我对每个字符串调用hashCode(),则返回的值将是连续的

现在,根据我对数据结构101的记忆,我觉得这是件坏事。因为假设HashMap通过如下算法选择存储桶:

class HashMap {
    private int capacity;
    private int chooseBucket(String key) {
        return key.hashCode() % capacity;
    }
}

这意味着相似的密钥存储在相邻的存储桶中,导致更高的冲突率,将大O查找时间从O(1)降低到…谁知道有多糟…可能比O(logn)更糟

对于我的第一个问题,我得到的答案大致是“这里不需要雪崩效应”、“它只用于加密哈希函数”和“字符串的哈希代码实现速度快,适用于小哈希映射”

这让我很困惑。当数据结构很小时,它们都很快。Sun不是提供了一个默认的hashCode函数,可以很好地用于大型数据集吗?这就是HashMap的性能真正重要的时候了,不是吗


或者,我错过了什么?请告诉我。

在连续存储桶中存储密钥不会导致性能下降。将钥匙存储在同一个存储桶(例如)中不起作用。使用链接解决哈希冲突时:

  • 最坏的情况是:每个散列值都相同,因此所有元素都在同一个bucket中,在这种情况下,您将获得O(n)性能(假设链是链表)
  • 最佳情况:每个散列值都是不同的,因此每个元素都位于不同的存储桶中,因此您可以获得预期的O(1)性能


散列表(及类似的)中使用的散列码不需要一个散列函数。

散列图之类的散列函数对于它的键集需要合理地唯一,但键之间的关系(即两个键的相似程度)不需要是随机的。我们真正想要避免的是在一个桶中有一堆对象,这会使搜索该桶变得昂贵

在HashMaps和string的情况下,它必须将这些散列键映射到一个随机可访问的容器中,比如一个数组,该数组有许多解决方案,但是如果两个键“接近”,它仍然会导致它们被放在不同的桶中,这正是我们真正关心的

对于非常大的映射容器(想想数十亿个键),我们可能确实希望变得更聪明一点,但这似乎超出了Java HashMap的设计目的


最后一点,您不必使用雪崩效应为字符串生成相当随机的键。你想选择一个足够随机且尽可能快的函数。

前几天我读了Eric Lippert的一篇博客文章,标题是。尽管代码示例与C#相关,但大多数一般原则同样适用于Java。如果您想更多地了解哈希代码的用途以及如何生成它们,那么本文非常值得一读

特别是,以下内容似乎与您的问题特别相关:

指南:散列码的分布必须是“随机的”

所谓“随机分布”,我的意思是,如果被散列的对象中存在共性,那么在生成的散列代码中就不应该存在类似的共性


如果您查看HashMap的源代码,就会发现有一个使用key.hashCode()值调用的hash函数,这意味着它会按照自己的方式分配一个hash。需要确定的一点是不要遵守equals和hashcode契约。如果您希望提高性能,我建议您查看源代码,了解可用存储桶的数量和最佳使用情况。

您会问“或者,我遗漏了什么吗?请告诉我。”

是的,你错过了一些东西

在HashMap类实现中,它可以防止糟糕的哈希函数:

/**
 * 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);
}
因此,在您的示例中生成的哈希代码是:

k1 - Before: 3366 After: 3566
k2 - Before: 3367 After: 3567
k3 - Before: 3368 After: 3552
因此,即使在你的3个元素的小样本中,其中一个元素被重新灰化了。现在,这并不能防止恶意的哈希代码(
return randomInt();
return 4;
根本无法防止),但它确实可以防止写得不好的哈希代码

我还应该指出,通过使用非平凡的输入,您可以极大地改变事情。例如,考虑以下字符串。

k1longer - Before: 1237990607 After: 1304548342
k2longer - Before: 2125494288 After: 2040627866
k3longer - Before: -1281969327 After: -1178377711
注意低位有多大的不同:对于哈希代码来说,唯一重要的是低位。背景图的大小始终是2的幂。实际上,代码中是这样记录的:

/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry[] table;
重新灰化在确保高位(在哈希表中通常被忽略)仍然对低位有影响方面做得相当不错。以下是原始哈希代码位置及其影响的位的映射:

00: 00000000000000000000000000000001
01: 00000000000000000000000000000010
02: 00000000000000000000000000000100
03: 00000000000000000000000000001000
04: 00000000000000000000000000010001
05: 00000000000000000000000000100010
06: 00000000000000000000000001000100
07: 00000000000000000000000010001001
08: 00000000000000000000000100010010
09: 00000000000000000000001000100100
10: 00000000000000000000010001001000
11: 00000000000000000000100010010000
12: 00000000000000000001000100100001
13: 00000000000000000010001001000010
14: 00000000000000000100010010000100
15: 00000000000000001000100100001000
16: 00000000000000010001001000010001
17: 00000000000000100010010000100010
18: 00000000000001000100100001000100
19: 00000000000010001001000010001001
20: 00000000000100010010000100010011
21: 00000000001000100100001000100110
22: 00000000010001001000010001001100
23: 00000000100010010000100010011000 # means a 1 in the 23rd bit position will  
24: 00000001000100100001000100110001  # cause positions 4, 5, 8, 12, and 20 to 
25: 00000010001001000010001001100010  # also be altered
26: 00000100010010000100010011000100
27: 00001000100100001000100110001001
28: 00010001001000010001001100010010
29: 00100010010000100010011000100100
30: 01000100100001000100110001001000
31: 10001001000010001001100010010000
因此,您对“将大O查找时间从O(1)降低到…谁知道有多糟…可能比O(logn)更糟”和“Sun是否会提供一个适用于大型数据集的默认hashCode函数?”的担忧可能会得到缓解-他们有适当的保护措施来防止这种情况发生

如果这有助于你获得一些平静,下面是这个类的作者标签。他们实际上是爪哇世界的明星。(带#的评论是我的)


“相似的密钥存储在相邻的存储桶中,导致更高的冲突率”——为什么这会导致更多的冲突?我不认为这句话有助于回答这个问题。在我看来,它听起来确实希望哈希代码中出现雪崩效应,这对于哈希表来说是不必要的。我有点同意
 * @author  Doug Lea          # Formerly a Java Community Process Executive Committee member
 * @author  Josh Bloch        # Chief Java architect at Google, amongst other things
 * @author  Arthur van Hoff   # Done too many hardcore Java things to list...
 * @author  Neal Gafter       # Now a lead on the C# team at Microsoft, used to be team lead on javac