Set<Double>.contains()是否可以设置精度?

8
假设我们有一个Set<Double>的实现,它包含以下值:[2.0, 5.0, 7.0]
在这种情况下,调用contains(2.0001d)会返回false,因为double值是通过精确匹配进行比较的。
对于boolean contains(Object o)方法,是否可以设置一些双精度精度呢?
如果不可能,除了将值存储在连续集合中、遍历它并比较每个值之外,您有什么解决方法建议?

你使用的是哪种 Set 实现? - akortex
@Aris_Kortex 我正在使用 HashSet - Andrii Lisun
你的精度是多少? - akortex
@Aris_Kortex,仅从理论上讲,我想要有一个选项来设置Set<Double>.contains()的任意精度。 - Andrii Lisun
请查看下面的答案。 - akortex
4个回答

6

Set.contains基于相等性有一个精确的定义:

更正式地说,如果此集合包含一个元素e使得(o == null ? e == null : o.equals(e)),则仅当返回true

如果使用除了相等性之外的任何东西,它都会违反方法的契约。而相等性具有明确定义,它必须是可传递的(在其他属性中)。使用公差的相等性方法不是可传递的。

因此,Set.contains没有办法允许公差。

然而,这并不意味着您永远不应该检查某个集合是否包含某个值与某个值的公差之内 - 只是不要尝试重载contains的概念来实现它。

例如,您可以编写一个接受NavigableSet(例如TreeSet)并使用其subSet方法的方法:

static boolean containsApprox(NavigableSet<Double> set, double target, double eps) {
  return !set.subSet(target - eps, true, target + eps, true).isEmpty();
}

这仅请求从 target-epstarget+eps(包括,如 true 参数所示)的集合部分。如果这是非空的,则在 eps 范围内有一个集合值等于 target

这显然是与标准的 Set.contains 不同的概念,因此对于这个含有不同属性的 contains 检查,是可以的。

你不能使用 HashMap 来进行相同的 subSet 技巧,因为它是一个无序映射 - 没有有效的方式来提取给定范围内的值。你需要像 Sun 的答案 中那样遍历整个集合,寻找匹配的值。


我喜欢子集的想法,这肯定比我的建议更好。 - assylias
@assylias非常同意,这非常好! - Eugene

3

也许您可以使用anyMatch,例如,基于 . 后的前两个数字进行比较:

Set<Double> set = Set.of(2.0, 5.0, 7.0);

Double compared = 2.0001d;

System.out.println(
        set.stream().anyMatch(aDouble -> 
                Math.floor(aDouble * 100) / 100 == Math.floor(compared * 100) / 100
));

2
我可以看到三个选项:
  • 将数字四舍五入后再加入集合
  • 编写一个double封装类并重新定义equals方法
  • 使用带有自定义比较器的TreeSet

请注意,最后两个选项可能或可能不令人满意,因为您填充集合的顺序会影响哪些元素被保留。例如,如果将精度设置为0.01,然后添加0.01、0.011和0.02,则集合中将有两个元素(0.01和0.02)。如果添加0.011、0.01、0.02,则只有一个元素:0.011。我不知道这是否符合您的用例。

最后一个选项可能如下所示:

static Set<Double> setWithPrecision(double epsilon) {
  return new TreeSet<> ((d1, d2) -> {
    if (d1 <= d2 - epsilon) return -1;
    if (d1 >= d2 + epsilon) return 1;
    return 0;
  });
}

使用示例:

Set<Double> set = setWithPrecision(0.01);
set.add(0d);
set.add(0.00001d);
set.add(0.01d);
set.add(0.02d);

System.out.println(set); // [0.0, 0.01, 0.02]

不错。但是他可以通过使用“BigDecimal”更轻松地设置传入的Double数字的精度。 - akortex
@Aris_Kortex 当然可以,或者在将它们添加到集合之前先将双精度数四舍五入 - 我已经添加了这个选项。 - assylias
是的,我将使用BigDecimal提供四舍五入过程给他。 - akortex
不不不不不。具有公差的相等和比较方法是不可传递的,因此违反了这些方法的契约(如果你要实现hashCode以与equals一致,除了使其返回一个固定值外,你还能怎么做)。请不要提倡2或3作为解决方案,这会导致非常令人惊讶的错误。而且1也不可传递。 - Andy Turner
@AndyTurner 嗯,说实话,你提出了一个非常有价值的观点。 - akortex
@AndyTurner同意。我已经添加了一条注释。我认为选项1是可以的,因为您可以保持集合中每个值的“规范”表示。 - assylias

0
假设您的HashSet#contains调用点在某个方法中,该方法以某种方式从某处接收到一个Double,您可以直接设置Double的精度。虽然Double没有提供一种开箱即用的方法来舍入Double对象,但您完全可以创建一个舍入助手,它将接受您想要实现的精度并返回舍入后的数字。

例如:

public static Double roundToPrecision(Double number, int precision) {
    return BigDecimal.valueOf(number)
        .setScale(precision, RoundingMode.DOWN)
        .doubleValue();
}

传入2.0001d将有效地导致其四舍五入为2.0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接