【问题标题】:Double in HashMapHashMap 中的双倍
【发布时间】:2009-07-02 14:38:53
【问题描述】:

我正在考虑使用 Double 作为 HashMap 的键,但我知道浮点比较是不安全的,这让我开始思考。 Double 类的 equals 方法是否也不安全?如果是这样,则意味着 hashCode 方法可能也不正确。这意味着使用 Double 作为 HashMap 的键会导致不可预知的行为。

谁能证实我的猜测?

【问题讨论】:

    标签: java hashcode


    【解决方案1】:

    简答:不要这样做

    长答案:这是计算密钥的方式:

    实际的键是java.lang.Double 对象,因为键必须是对象。这是它的hashCode() 方法:

    public int hashCode() {
      long bits = doubleToLongBits(value);
      return (int)(bits ^ (bits >>> 32));
    }
    

    doubleToLongBits() 方法基本上占用 8 个字节并表示它们的长度。因此,这意味着 double 计算中的微小变化可能意味着很大,并且您将有关键失误。

    如果您可以满足点后给定的点数 - 乘以 10^(点后的位数)并转换为 int(例如 - 2 位数乘以 100)。

    这样会更安全。

    【讨论】:

    • 就像 Jay 和 Carlos 在他们的回答中所说的那样,这是一个 equals 问题,而不是哈希码。
    【解决方案2】:

    我认为你是对的。虽然双打的散列是整数,但双打可能会弄乱散列。这就是为什么,正如 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 的哈希码已经使用了这种精确的方法,除了 17 和 37 部分。 "long bits = doubleToLongBits(value); return (int)(bits ^ (bits >>> 32));"
    • @Tom,你是在暗示Double.hashCode 坏了吗?也就是说,对于两个等于Doubles,它们可能有不同的哈希码?
    【解决方案3】:

    这取决于你将如何使用它。

    如果您对只能根据完全相同的位模式(或可能等效的位模式,例如 +/- 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。

    这个定义允许哈希表 正常运行。

    不过,您很可能对“非常接近键的数字”感兴趣,这使得它的可行性大大降低。特别是如果您要进行一组计算以获取一次密钥,然后进行另一组计算以获取第二次密钥,您将遇到问题。

    【讨论】:

    【解决方案4】:

    问题不在于哈希码,而在于双精度。这会导致一些奇怪的结果。示例:

        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)
    

    它应该找到条目... []]

    【讨论】:

    • 由于两个非常相似的数字的 hashCode 可能不同,您甚至可能没有在正确的存储桶中查找。
    • 我写的,不是吗?但这不是问题,因为 equals 无论如何都会返回 false (如果数字 very very 相似)
    【解决方案5】:

    简短的回答:它可能行不通。

    诚实的回答:这一切都取决于。

    更长的答案:哈希码不是问题,而是浮点相等比较的本质。正如 Nalandial 和他帖子中的评论者所指出的那样,最终任何与哈希表的匹配最终仍会使用 equals 来选择正确的值。

    所以问题是,你的双打是否以你知道等于真的等于等于的方式生成?如果您读取或计算一个值,将其存储在哈希表中,然后使用完全相同的计算读取或计算该值,则 Double.equals 将起作用。但否则它是不可靠的:1.2 + 2.3 不一定等于 3.5,它可能等于 3.4999995 或其他。 (不是一个真实的例子,我只是编造的,但这就是发生的事情。)您可以合理可靠地比较浮点数和双精度数,用于小于或大于,但不能用于等于。

    【讨论】:

      【解决方案6】:

      也许BigDecimal 可以带你去你想去的地方?

      【讨论】:

        【解决方案7】:

        使用双精度的哈希,而不是双精度本身。

        编辑:谢谢,乔恩,我其实不知道。

        对此我不确定(您应该只查看 Double 对象的源代码),但我认为浮点比较的任何问题都会为您解决。

        【讨论】:

        • 不,最初使用哈希来查找存储桶,然后使用 equals。
        • 哈希用于查找具有相等哈希的值列表所在的“桶”。一旦找到桶,键值对就会迭代以查找“相等”的键' 并返回相应的值。
        • hashCode and equals on Double 使用 IEE 754 浮点数位表示
        • 两个不相等的对象可以有相同的哈希码,因此,map 倾向于用等号对键进行双重检查,所以这非常重要。
        【解决方案8】:

        这取决于您存储和访问地图的方式,是的,相似的值最终可能会略有不同,因此不会散列到相同的值。

        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);
        

        会很危险

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-10-31
          • 2014-02-28
          • 1970-01-01
          • 1970-01-01
          • 2013-03-30
          • 2019-01-08
          • 2013-05-07
          • 2014-03-02
          相关资源
          最近更新 更多