浮点误差确实很烦人。在开发下一版本的Point时,我理解了它的真实含义(这次我实际上是在愚弄我的代码)。在上载它进行审查之前,我希望对我编写的名为Numbers的类进行审查。

Numbers.java

package library.util;

/**
 * This class provides a useful interface for easy (approximate) floating
 * point equality checks. This is necessary for several other classes.
 * 
 * @author Subhomoy Haldar (ambigram_maker)
 * @version 1.0
 */
public class Numbers {

    /**
     * The tolerance value for comparing the {@code float} values.
     */
    public static double FLOAT_TOLERANCE = 5E-8;
    /**
     * The tolerance value for comparing the {@code double} values.
     */
    public static double DOUBLE_TOLERANCE = 5E-16;

    /**
     * Returns {@code true} if the arguments are <i>exactly</i> equal.
     *
     * @param bytes The arguments to check.
     * @return {@code true} if the arguments are <i>exactly</i> equal.
     */
    public static boolean areEqual(byte... bytes) {
        int length = bytes.length;
        checkLength(length);
        byte d = bytes[0];
        for (int i = 1; i < length; i++) {
            if (d != bytes[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the arguments are <i>exactly</i> equal.
     *
     * @param shorts The arguments to check.
     * @return {@code true} if the arguments are <i>exactly</i> equal.
     */
    public static boolean areEqual(short... shorts) {
        int length = shorts.length;
        checkLength(length);
        short d = shorts[0];
        for (int i = 1; i < length; i++) {
            if (d != shorts[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the arguments are <i>exactly</i> equal.
     *
     * @param ints The arguments to check.
     * @return {@code true} if the arguments are <i>exactly</i> equal.
     */
    public static boolean areEqual(int... ints) {
        int length = ints.length;
        checkLength(length);
        int d = ints[0];
        for (int i = 1; i < length; i++) {
            if (d != ints[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the arguments are <i>exactly</i> equal.
     *
     * @param longs The arguments to check.
     * @return {@code true} if the arguments are <i>exactly</i> equal.
     */
    public static boolean areEqual(long... longs) {
        int length = longs.length;
        checkLength(length);
        long d = longs[0];
        for (int i = 1; i < length; i++) {
            if (d != longs[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the arguments are <i>exactly</i> equal.
     *
     * @param chars The arguments to check.
     * @return {@code true} if the arguments are <i>exactly</i> equal.
     */
    public static boolean areEqual(char... chars) {
        int length = chars.length;
        checkLength(length);
        char d = chars[0];
        for (int i = 1; i < length; i++) {
            if (d != chars[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the arguments are <i>approximately</i> equal.
     *
     * @param floats The arguments to check.
     * @return {@code true} if the arguments are <i>approximately</i> equal.
     */
    public static boolean areEqual(float... floats) {
        int length = floats.length;
        checkLength(length);
        float d = floats[0];
        for (int i = 1; i < length; i++) {
            if (!areEqual(d, floats[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the arguments are <i>approximately</i> equal.
     *
     * @param doubles The arguments to check.
     * @return {@code true} if the arguments are <i>approximately</i> equal.
     */
    public static boolean areEqual(double... doubles) {
        int length = doubles.length;
        checkLength(length);
        double d = doubles[0];
        for (int i = 1; i < length; i++) {
            if (!areEqual(d, doubles[i])) {
                return false;
            }
        }
        return true;
    }

    private static void checkLength(int length) throws
            IllegalArgumentException {
        if (length < 2) {
            throw new IllegalArgumentException
                    ("At least two arguments required.");
        }
    }

    private static boolean areEqual(float f1, float f2) {
        // the corner cases first:
        if (Float.isNaN(f1) && Float.isNaN(f2)) {
            return true;
        }
        if (Float.isInfinite(f1) || Float.isInfinite(f2)) {
            return f1 == f2;
        }
        float abs;
        if (f1 == f2 || (abs = Math.abs(f1 - f2)) <= FLOAT_TOLERANCE) {
            return true;
        }
        // compare using the larger ulp
        float ulp1 = Math.ulp(f1);
        float ulp2 = Math.ulp(f2);
        return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
    }

    private static boolean areEqual(double d1, double d2) {
        // the corner cases first:
        if (Double.isNaN(d1) && Double.isNaN(d2)) {
            return true;
        }
        if (Double.isInfinite(d1) || Double.isInfinite(d2)) {
            return d1 == d2;
        }
        double abs;
        if (d1 == d2 || (abs = Math.abs(d1 - d2)) <= DOUBLE_TOLERANCE) {
            return true;
        }
        // compare using the larger ulp
        double ulp1 = Math.ulp(d1);
        double ulp2 = Math.ulp(d2);
        return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
    }
}


我希望您尽全力并检查实现的缺点(尤其是floatdouble方法)。

Numbers.java的原因(和用法)


我建议使用以下方法:

boolean example = Numbers.areEqual(60, Math.toDegrees(Math.acos(0.5)));


(我认为)这很容易阅读和理解它的作用。

评论

请注意,浮点算术不是不准确的。它只是不符合人们对实数的期望。您必须按照自己的意愿接受它,就像您接受(1/3)* 3不是1一样。

@PeteBecker是的,但是所有程序都需要一种变通的方法来满足人们的期望吗?您是否愿意使用图书馆来简化生活?

不,您必须像使用整数算术一样调整期望值。问题是大多数人(合法地)不了解浮点算法的作用。 “几乎平等”试图通过假装事实并非如此来解决这种缺乏理解的情况。

我完全同意。但是,如果尝试说tan 90不是无限,我肯定会被踢出课堂。 ;-)

tan 90是无限(宽松地说),tanπ/ 2也是如此;这些是关于实数的陈述。 tan(3.14159 / 2)可能是一个很大的有限值;那是关于浮点运算的声明。

#1 楼

您关于平等的数学标准存在一些问题。

要考虑的重要一点是,您的“平等”不是可传递的。在代码中,您使用的阈值为\ $ \ epsilon \ $(当值的类型为double时,在代码中使用\ $ \ epsilon = 5 \ times10 ^ {-16} \ $);如果\ $ | x-y | \ leq \ epsilon \ $,则声明\ $ x \ $和\ $ y \ $彼此相等。现在假设您有三个值\ $ a \ $,\ $ b \ $和\ $ c \ $,使得\ $ b = a + 0.9 \ epsilon \ $和\ $ c = a-0.9 \ epsilon \ $。在这种情况下,areEqual(a, b, c)将返回true,但是areEqual(b, a, c)将返回false。这是令人惊讶的行为-areEqual()的文档没有说结果取决于参数的顺序。

要解决某些症状,您需要比较所有值对:areEqual(a, b, c)应返回仅当trueareEqual(a, b)areEqual(a, c)全部返回areEqual(b, c)时,true。一般而言,这是二次的:使用10个参数,您最终进行了45次比较。具有20个参数和190个比较...这可能会在计算上令人望而却步。尽管这可以避免结果取决于参数顺序的问题,但仍不会使关系传递:areEqual(a, b)areEqual(a, c)可以返回true,而areEqual(b, c)可以返回false


而且,使用差异通常不是近似的“正确”方法。如果\ $ x = 10 ^ {-30} \ $和\ $ y = 10 ^ {-36} \ $,则通常不应将它们视为相等,甚至近似,因为\ $ x \ $是一百万比\ $ y \ $大一倍-但您的areEqual()会声明它们彼此相等。您会遇到大数字的双重问题:当\ $ x \ $大时(double值接近\ $ 2 ^ {53} \ $),\ $ x \ $和\ $ x + 1 \ $(例如)差等于\ $ 1 \ $(因此大于\ $ \ epsilon \ $),但实际上它们的最低有效位只是不同;您使用Math.ulp()显式添加了一些代码来处理这种情况。

在某些使用上下文中,差异才是最有意义的。但一般来说,比率是更合适的。在物理学中,值是度量,具有给定的精度,该精度或多或少是有效数字的数量。比率反映了这一概念。在数学上,这意味着如果\ $ |(x / y)-1 |,则声明\ $ x \ $和\ $ y \ $彼此近似相等。 \ leq \ epsilon \ $(还有\ $ |(y / x)-1 | \ leq \ epsilon \ $,使该关系至少对称)。这不能解决所有情况,您必须对非常接近\ $ 0 \ $的值做一些事情;但是该标准将是“近似相等”的更正确概念,它将平滑地上下扩展,并避免使用Math.ulp()进行繁琐的交易。

评论


\ $ \ begingroup \ $
欢迎使用代码审查!这是一个无代码答案的非常好的示例,感谢您的贡献:-)
\ $ \ endgroup \ $
– Mathieu Guindon♦
2015年9月2日,下午1:06

\ $ \ begingroup \ $
第一次阅读您的答案时,我试图捍卫自己的实现。我第二次开始思考。我第三次因为自己是个白痴而受诅咒。我一定会接受您的建议。顺便说一句,公差可以由用户根据需要进行修改。因此,如果用户这样做:Numbers.DOUBLE_TOLERANCE = 1e-40 ;,则Numbers.areEqual(1e-30,1e-36)返回false。
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月2日,下午6:06

\ $ \ begingroup \ $
“总的来说,这是二次方的”-寻找线性算法并不困难;首先处理像NaN和无穷大这样的边缘情况,然后返回max(a,b,c)-min(a,b,c) \ $ \ endgroup \ $
–塔米尔
2015年9月2日,9:10



\ $ \ begingroup \ $
我还要指出,OP的实现将NaN等同于NaN-这违反了浮点标准。
\ $ \ endgroup \ $
–塔米尔
2015年9月2日,9:15

#2 楼


您真的需要一个新的实现进行浮点数比较吗? java.math.BigDecimal可能通过其compareTo(arg)方法对FP编号比较有用。

areEqual(args...)重载似乎过于重复。您可以考虑使用单一方法中的泛型来重新定义它们。一个示例,其中原始代码略有更改:

public static <T extends Number> boolean areEqual(T... numbers) {
    int length = numbers.length;
    checkLength(length);
    T d = numbers[0];
    for (int i = 1; i < length; i++) {
        if (!d.equals(numbers[i])) {
            return false;
        }
    }
    return true;
}


当然,在这种情况下,您将松开基元,因为它们将被装箱,但这将节省多行代码。



评论


\ $ \ begingroup \ $
嗨!欢迎使用代码审查!我可能建议您继续游览并阅读我们的帮助中心:-)。同时,通过正确设置列表格式,我使您的帖子更易于阅读。
\ $ \ endgroup \ $
– Ethan Bierlein
2015年9月1日于21:52

#3 楼

关于FLOAT_TOLERANCEDOUBLE_TOLERANCE的另一个小问题(比数学更多的编程问题):


如果Numbers的外部世界需要了解这些值,请将其定为最终值(避免有人对其进行修改并更改未来数字比较的正确性)
否则,将它们设为private


更新


public class Numbers {

    /**
     * The tolerance value for comparing the {@code float} values.
     */
    public static final float FLOAT_TOLERANCE = 5E-8f;
    /**
     * The tolerance value for comparing the {@code double} values.
     */
    public static final double DOUBLE_TOLERANCE = 5E-16;

    ...

    /**
     * Returns {@code true} if the arguments are <i>approximately</i> equal. The default delta used for comparisons is {@value #FLOAT_TOLERANCE}.
     *
     * @param floats The arguments to check.
     * @return {@code true} if the arguments are <i>approximately</i> equal.
     */
    public static boolean areEquals(float... floats) {
        return areEqual(FLOAT_TOLERANCE, floats);
    }

    /**
     * Returns {@code true} if the arguments are <i>approximately</i> equal.
     *
     * @param tolerance The delta for comparisons.
     * @param floats The arguments to check.
     * @return {@code true} if the arguments are <i>approximately</i> equal.
     */
    public static boolean areEqual(float tolerance, float... floats) {
        int length = floats.length;
        checkLength(length);
        float d = floats[0];
        for (int i = 1; i < length; i++) {
            if (!areEqual_impl(tolerance, d, floats[i])) {
                return false;
            }
        }
        return true;
    }

    private static boolean areEqual_impl(float tolerance, float f1, float f2) {
        // the corner cases first:
        if (Float.isNaN(f1) && Float.isNaN(f2)) {
            return true;
        }
        if (Float.isInfinite(f1) || Float.isInfinite(f2)) {
            return f1 == f2;
        }
        float abs;
        if (f1 == f2 || (abs = Math.abs(f1 - f2)) <= tolerance) {
            return true;
        }
        // compare using the larger ulp
        float ulp1 = Math.ulp(f1);
        float ulp2 = Math.ulp(f2);
        return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
    }

    //Same principle for double

}


评论


\ $ \ begingroup \ $
好吧,我打算为用户提供灵活性。就像我在Tom Leek的答案中评论的那样:“用户可以根据需要修改TOLERANCE。因此,如果用户这样做:Numbers.DOUBLE_TOLERANCE = 1e-40 ;,则Numbers.areEqual(1e-30,1e-36)返回假。”
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月2日,14:37

\ $ \ begingroup \ $
@ambigram_maker我用满足您要求(灵活性)的解决方案更新了答案,并通过添加考虑公差的方法使其更加可靠(无副作用,更易于测试)。 FLOAT_TOLERANCE和DOUBLE_TOLERANCE被声明为公共静态最终值(请注意最终值),以避免任何用户对其进行修改。
\ $ \ endgroup \ $
–发现
2015年9月2日在18:12

\ $ \ begingroup \ $
好吧,我喜欢这个主意。但是有一个陷阱:它。不。编译。 :-/
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月4日在9:47

\ $ \ begingroup \ $
另外,为什么要对FLOAT_TOLERANCE使用double?
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月4日在9:49

\ $ \ begingroup \ $
@ambigram_maker,您是对的,我没有注意到您在提供的代码中将FLOAT_TOLERANCE声明为double。我将其更改为浮动。
\ $ \ endgroup \ $
–发现
2015年9月4日在9:55

#4 楼

对我来说,这是干净的代码。

有很多重复的代码,但这是因为Java不支持基元的泛型。即使重复,也很容易阅读和理解。直截了当。

我认为ckeckLength()是此方法的错误名称。也许assertAtLeastTwoElements(array)或更具描述性的内容会更好。

浮点数的equals方法似乎是正确的。希望您已经编写了一些测试用例。

但是我想的最大的事情是极端情况。
NaN对我来说是一个错误情况。在数学上,您不能说1/0等于2/0,因为您实际上并不知道某个数字除以零是什么。
这取决于用例,但是通常,如果出现以下情况,应用程序应该爆炸并显示大错误消息:我认为计算的结果是NaN。

无穷大也是...嗯...
如果数学成立,则数学相等:\ $ x = y \ Leftrightarrow | x-y | = 0 \ $。这就是你在做什么。对于浮点:\ $ x = y \ Leftrightarrow | x-y | <\ epsilon \ $。但是对于无穷大,它是\ $ | \ infty-\ infty | = \ infty \ $(对于NaN也是如此)。所以它不成立。

无限用例可能会有一个更大的用例,它可能是所需的行为,因此可能很好,但是应记录此行为。

最后,您会说:漂亮干净的代码。干得好!

评论


\ $ \ begingroup \ $
是否需要取模? 1-1 = 0等于| 1-1 | = 10
\ $ \ endgroup \ $
– Caridorc
2015年9月1日22:21在

\ $ \ begingroup \ $
您看到... 1/0导致ArithmeticException,但1.0 / 0返回Double.POSITIVE_INFINITY。给出Double.NaN是0.0 / 0.0。
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月2日,下午4:10