为什么这个hashCode()方法被认为很差?

时间:2015-01-04 06:49:09

标签: java performance hashmap hashcode

这是" Using Java 7 HashMap in Java 8"的后续问题。有一些有趣的评论。有些我很清楚;别人少了。

为什么这个hashCode()方法被认为很差?

乍一看,我觉得这很合理。也许17可以增加到31.否则,它似乎遵循Arrays.hashCode(Object[])中普遍接受的公式。一个猜测:它适用于一般情况,其中项目数量相对较小(小于10.000),但对于非常大的集合(1.000.000或更高)表现不佳。

以下是原始代码:(包含所有内容以提供一些上下文。)

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class Test1 {

static int max_k1 = 500;
static int max_k2 = 500;

static Map<Node, Node> map;
static Random random = new Random();

public static void main(String[] args) {
    for (int i = 0; i < 15; i++) {
        long start = System.nanoTime();
        run();
        long end = System.nanoTime();
        System.out.println((end - start) / 1000_000);
    }
}

private static void run() {
    map = new HashMap<>();
    for (int i = 0; i < 10_000_000; i++) {
        Node key = new Node(random.nextInt(max_k1), random.nextInt(max_k2));
        Node val = getOrElseUpdate(key);
    }
}

private static Node getOrElseUpdate(Node key) {
    Node val;
    if ((val = map.get(key)) == null) {
        val = key;
        map.put(key, val);
    }
    return val;
}

private static class Node {

    private int k1;
    private int k2;

    public Node(int k1, int k2) {
        this.k1 = k1;
        this.k2 = k2;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + k1;
        result = 31 * result + k2;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;

        if (!(obj instanceof Node))
            return false;

        Node other = (Node) obj;

        return k1 == other.k1 && k2 == other.k2;
    }
  }
}

3 个答案:

答案 0 :(得分:6)

我是告诉你这是穷人的人之一。我告诉你原因:&#34; 250,000个Node值,它只有15969个哈希码。&#34;

如果您的Node项目应该在0≤k1&lt; 0 k2上分布均匀分布。 500和0≤k1&lt; 500范围,那么您有250,000个可能的节点值。

一个好的哈希函数应该为你提供这些250,000个值尽可能唯一的哈希码。也就是说,理想情况下,良好的哈希函数应该为k2500 * k1 + k2的每个组合提供不同的值。

散列函数不需要是唯一的,因为在许多情况下这是不可能的 - 如果你的对象具有数万亿和数万亿的可能组合,当然你不能将所有这些组合映射到不同的整数。

您使用的标准哈希函数适用于该类对象。如果你有均匀分布的对象具有多种可能性,那么这种散列函数最终将使用所有可能的整数值,并且这是最好的。

但在您的特定情况下,您有250,000个组合,可以使用函数Node轻松表示为单个整数。完全独特的哈希函数是理想的。

&#34;标准&#34;你使用的哈希函数表现不佳,因为在这么小的整数范围内,它将它们中的许多映射到相同的值,你最终只有15,969个唯一的哈希码。这意味着您的许多250,000/15,969对象将映射到相同的哈希码。 (每个代码{{1}}!)。所以你将会遇到很多哈希冲突。

您拥有的哈希冲突越多,哈希映射的性能就越差,因为大部分哈希映射都是如此。良好的性能依赖于相同散列桶中尽可能少的密钥。散列桶由散列码决定。

答案 1 :(得分:4)

您的哈希函数可以写成31 * 17 * 31 + 31 * k1 + k2。

您可以看到向k2添加31和向k1添加-1将产生相同的哈希值。

然后,大约1到500范围内的每一对数字都会有大约12个 (500/31)具有相同散列的其他对。

在示例代码中完美执行的哈希函数将是500 * k1 + k2。 (快速测试显示性能提升约3倍。)

正如路易斯·瓦瑟曼所指出的,使用了一位经过深思熟虑的将军 来自库的哈希函数可能是一个安全的选择。

至于为什么标准数组散列函数在这种情况下表现不佳(顺便说一句,IntelliJ默认生成相同的函数。)

这里不要求完整的分析,但是散列变量的数量明显更大(假设它们在某种意义上是独立的)并且每个可能值的集合越大,函数执行得越好。在你的情况下,性能很差,因为只有2个变量,它们都有很小的范围。

似乎在Java 8中,HashMap实现变得更加复杂,可能是在某些情况下针对更好的渐近性能进行了优化。这种较小的复杂性以及性能较差的散列函数会导致性能下降。

就此而言,linear probing hash map可能是一个更好的算法。由于结构更简单,缓存丢失更少,因此在读取繁重的工作负载时应该提供更好的性能。我自己对Java库感兴趣,提供了很好的通用线性探测哈希映射。

答案 2 :(得分:2)

问题是,当输入范围很小时,坦率地说,它不能很好地工作。当你有像Strings这样的东西时,它可以正常工作,但不适用于小的整数。

您可以考虑使用像Murmur这样的散列算法。如果您可以使用像Guava这样的第三方库,则可能是

return Hashing.murmur3_32().newHasher().putInt(k1).putInt(k2).hash().asInt();