这是" Using Java 7 HashMap in Java 8"的后续问题。有一些有趣的评论。有些我很清楚;别人少了。
为什么这个hashCode()
方法被认为很差?
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;
}
}
}
答案 0 :(得分:6)
我是告诉你这是穷人的人之一。我告诉你原因:&#34; 250,000个Node
值,它只有15969个哈希码。&#34;
如果您的Node
项目应该在0≤k1
&lt; 0 k2
上分布均匀分布。 500和0≤k1
&lt; 500范围,那么您有250,000个可能的节点值。
一个好的哈希函数应该为你提供这些250,000个值尽可能唯一的哈希码。也就是说,理想情况下,良好的哈希函数应该为k2
和500 * 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();