【发布时间】:2009-07-02 14:38:53
【问题描述】:
我正在考虑使用 Double 作为 HashMap 的键,但我知道浮点比较是不安全的,这让我开始思考。 Double 类的 equals 方法是否也不安全?如果是这样,则意味着 hashCode 方法可能也不正确。这意味着使用 Double 作为 HashMap 的键会导致不可预知的行为。
谁能证实我的猜测?
【问题讨论】:
我正在考虑使用 Double 作为 HashMap 的键,但我知道浮点比较是不安全的,这让我开始思考。 Double 类的 equals 方法是否也不安全?如果是这样,则意味着 hashCode 方法可能也不正确。这意味着使用 Double 作为 HashMap 的键会导致不可预知的行为。
谁能证实我的猜测?
【问题讨论】:
简答:不要这样做
长答案:这是计算密钥的方式:
实际的键是java.lang.Double 对象,因为键必须是对象。这是它的hashCode() 方法:
public int hashCode() {
long bits = doubleToLongBits(value);
return (int)(bits ^ (bits >>> 32));
}
doubleToLongBits() 方法基本上占用 8 个字节并表示它们的长度。因此,这意味着 double 计算中的微小变化可能意味着很大,并且您将有关键失误。
如果您可以满足点后给定的点数 - 乘以 10^(点后的位数)并转换为 int(例如 - 2 位数乘以 100)。
这样会更安全。
【讨论】:
我认为你是对的。虽然双打的散列是整数,但双打可能会弄乱散列。这就是为什么,正如 Josh Bloch 在 Effective Java 中提到的那样,当您使用 double 作为哈希函数的输入时,您应该使用 doubleToLongBits()。同样,使用 floatToIntBits 表示浮点数。
特别是,要按照 Josh Bloch 的食谱使用双精度作为哈希,您应该这样做:
public int hashCode() {
int result = 17;
long temp = Double.doubleToLongBits(the_double_field);
result = 37 * result + ((int) (temp ^ (temp >>> 32)));
return result;
}
这来自 Effective Java 的第 8 条,“当你覆盖 equals 时,总是覆盖 hashCode”。可以在this pdf of the chapter from the book找到。
希望这会有所帮助。
【讨论】:
Double.hashCode 坏了吗?也就是说,对于两个等于Doubles,它们可能有不同的哈希码?
这取决于你将如何使用它。
如果您对只能根据完全相同的位模式(或可能等效的位模式,例如 +/- 0)找到值感到满意和各种NaN),那么它可能没问题。
特别是,所有的 NaN 最终都将被视为相等,但 +0 和 -0 将被视为不同。来自Double.equals 的文档:
请注意,在大多数情况下,对于两个 Double、d1 和 d2 类的实例, d1.equals(d2) 的值为真,如果 只有当
d1.doubleValue() == d2.doubleValue() 也有值 真的。然而,有两个 例外:
- 如果 d1 和 d2 都代表 Double.NaN,然后是 equals 方法 返回真,即使 Double.NaN==Double.NaN 有值 假的。
- 如果 d1 表示 +0.0 而 d2 表示 -0.0,反之亦然,则 equal test 的值为 false,甚至 虽然 +0.0==-0.0 的值为 true。
这个定义允许哈希表 正常运行。
不过,您很可能对“非常接近键的数字”感兴趣,这使得它的可行性大大降低。特别是如果您要进行一组计算以获取一次密钥,然后进行另一组计算以获取第二次密钥,您将遇到问题。
【讨论】:
问题不在于哈希码,而在于双精度。这会导致一些奇怪的结果。示例:
double x = 371.4;
double y = 61.9;
double key = x + y; // expected 433.3
Map<Double, String> map = new HashMap<Double, String>();
map.put(key, "Sum of " + x + " and " + y);
System.out.println(map.get(433.3)); // prints null
计算的值(键)是“433.29999999999995”,它不等于 433.3,因此您在 Map 中找不到条目(哈希码可能也不同,但这不是主要问题)。
如果你使用
map.get(key)
它应该找到条目... []]
【讨论】:
简短的回答:它可能行不通。
诚实的回答:这一切都取决于。
更长的答案:哈希码不是问题,而是浮点相等比较的本质。正如 Nalandial 和他帖子中的评论者所指出的那样,最终任何与哈希表的匹配最终仍会使用 equals 来选择正确的值。
所以问题是,你的双打是否以你知道等于真的等于等于的方式生成?如果您读取或计算一个值,将其存储在哈希表中,然后使用完全相同的计算读取或计算该值,则 Double.equals 将起作用。但否则它是不可靠的:1.2 + 2.3 不一定等于 3.5,它可能等于 3.4999995 或其他。 (不是一个真实的例子,我只是编造的,但这就是发生的事情。)您可以合理可靠地比较浮点数和双精度数,用于小于或大于,但不能用于等于。
【讨论】:
也许BigDecimal 可以带你去你想去的地方?
【讨论】:
使用双精度的哈希,而不是双精度本身。
编辑:谢谢,乔恩,我其实不知道。
对此我不确定(您应该只查看 Double 对象的源代码),但我认为浮点比较的任何问题都会为您解决。
【讨论】:
这取决于您存储和访问地图的方式,是的,相似的值最终可能会略有不同,因此不会散列到相同的值。
private static final double key1 = 1.1+1.3-1.6;
private static final double key2 = 123321;
...
map.get(key1);
会很好,但是
map.put(1.1+2.3, value);
...
map.get(5.0 - 1.6);
会很危险
【讨论】: