为什么 BigDecimal 自然排序与 equals 不一致?

2022-09-01 19:27:27

来自 Javadoc for BigDecimal

注意:如果对象被用作 a 中的键或 a 中的元素,则应小心谨慎,因为 的自然排序与 equals 不一致BigDecimalSortedMapSortedSetBigDecimal

例如,如果创建 a 并添加并添加到其中,则该集合将包含两个元素(因为值具有不同的刻度,因此根据 和 )不相等,但是如果对 执行相同的操作,则该集合将仅包含一个元素,因为在使用 时值的比较相等。HashSetnew BigDecimal("1.0")new BigDecimal("1.00")equalshashCodeTreeSetcompareTo

这种不一致背后有什么具体原因吗?


答案 1

来自 BigDecimal 的 OpenJDK 实现

/**
     * Compares this {@code BigDecimal} with the specified
     * {@code Object} for equality.  Unlike {@link
     * #compareTo(BigDecimal) compareTo}, this method considers two
     * {@code BigDecimal} objects equal only if they are equal in
     * value and scale (thus 2.0 is not equal to 2.00 when compared by
     * this method).
     *
     * @param  x {@code Object} to which this {@code BigDecimal} is 
     *         to be compared.
     * @return {@code true} if and only if the specified {@code Object} is a
     *         {@code BigDecimal} whose value and scale are equal to this 
     *         {@code BigDecimal}'s.
     * @see    #compareTo(java.math.BigDecimal)
     * @see    #hashCode
     */
    @Override
    public boolean equals(Object x) {
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
    if (scale != xDec.scale)
        return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) {
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
        } else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);

        return this.inflate().equals(xDec.inflate());
    }

更多来自实现:

 * <p>Since the same numerical value can have different
 * representations (with different scales), the rules of arithmetic
 * and rounding must specify both the numerical result and the scale
 * used in the result's representation.

这就是为什么实施要考虑的原因。将字符串作为参数的构造函数实现如下:equalsscale

    public BigDecimal(String val) {
        this(val.toCharArray(), 0, val.length());
    }

其中第三个参数将用于(在另一个构造函数中),这就是为什么字符串和将创建不同的BigDecimals(具有不同的刻度)。scale1.01.00

来自 Effective Java by Joshua Bloch:

compareTo合约的最后一段,这是一个强有力的建议,而不是一个真正的规定,它只是说,由compareTo方法施加的相等性测试通常应该返回与equals方法相同的结果。如果遵守这一规定,则 compareTo 方法强加的顺序称为与 equals 一致。如果违反,则称排序与 equals 不一致。其 compareTo 方法施加的顺序与 equals 不一致的类仍将有效,但包含该类元素的排序集合可能不遵守相应集合接口(集合、集或映射)的一般协定。这是因为这些接口的一般协定是根据 equals 方法定义的,但排序的集合使用 compareTo 施加的相等性测试来代替 equals。如果发生这种情况,这不是一场灾难,但这是需要注意的。


答案 2

在算术精度的上下文中,这种行为似乎是合理的,其中尾随零是有效数字,1.0 不具有与 1.00 相同的含义。使它们不平等似乎是一个合理的选择。

但是,从比较的角度来看,两者中的任何一个都不大于或小于另一个,并且可比接口需要一个总顺序(即每个BigDecimal必须与任何其他BigDecimal具有可比性)。这里唯一合理的选择是定义一个总顺序,以便 compareTo 方法将两个数字视为相等。

请注意,equal 和 compareTo 之间的不一致不是问题,只要它被记录下来。它甚至有时正是人们所需要的


推荐