我正在尝试了解hashCode
背后的全部故事。在大多数实现中,hashCode
是完全确定性的,例如在StringUTF16
类中:
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h;
}
我认为这样的实现不是很好:构建具有相同hashCode的示例很容易。例如,对于DOS攻击,系统用户可以提交具有相同hashCode
的单词。它无法实现String
,因为它实现了Comparable
(并且HashMap
是一个被黑客入侵的烂摊子),但是对于没有实现{{ 1}}。
更好的方法似乎是使用随机因子(而不是Comparable
),以使用户不知道如何构造不良示例(并且还具有一些理论属性),如下所示:>
31
现在,我的问题是:此实现有什么不好的地方吗?我能看到的唯一问题是,它将为程序的不同运行返回不同的hashCode,但我无法想象出现错误的具体情况。
答案 0 :(得分:3)
除非您进入专门的序列化应用程序,否则我认为这并不是什么大问题。在大多数情况下,就其运行时而言,设置方式基本上等同于添加任意31
值(该值不会更改)。
尽管如此,通过反射“诡计多端”,您可以潜在地更改该值并使整个系统偏离轨道(请考虑setAccessible
和修饰符标志)。
如果将对象序列化并转移到不同环境中时,有一种设置取决于哈希码和一致性,那么我发现出现问题的机会更大。散列码在两个不同环境之间进行比较的方式很有可能在实际上不应该有所不同的情况下发生变化。
答案 1 :(得分:3)
不要求hashCode
在不同的JVM中提供相同的值。例如,HashMap
类在序列化时不会保留映射键的hashCode
值。而是在反序列化地图时重新计算hashCode
值。
我看到的唯一潜在问题是,每次调用时重新计算hashCode
效率很低。您可以通过延迟计算来解决(例如,String::hashCode
就是这样)。
但是,如果您实现延迟hashCode
计算,则需要声明将字段存储为transient
。否则,解持久键实例中的hashCode
值将不会==
为与该键“相等”的另一个实例计算的hashCode
值。 (换句话说,hashcode / equals contract已损坏!)这将导致查找失败。
如果正确执行此操作,则对HashMap
的序列化应该没有问题。例如,您可以采用String::hashCode
的方法,并使用零作为缓存的hashCode
值,这意味着hashCode()
方法的“代码需要计算”。
(如果您的键类没有用于保存缓存的hashCode
值的字段,则不会出现持久保留该值的问题。)
要注意的另一件事是,修改要实现Comparable
的密钥类将是针对基于DOS的攻击的另一种防御措施。在示例类中,compareTo
方法的实现很简单。注意,实现的顺序在语义上不需要有意义。它只需要保持稳定和一致即可。