在Java中操作和比较浮点数

10

在Java中,浮点数的算术运算并不精确。例如,以下Java代码:

float a = 1.2; 
float b= 3.0;
float c = a * b; 
if(c == 3.6){
    System.out.println("c is 3.6");
} 
else {
    System.out.println("c is not 3.6");
} 

输出:"c不是3.6"。

我对小数点后三位以外的精度不感兴趣(#.###)。如何处理浮点数乘法并可靠地进行比较?


1
声明浮点数使用:float a = 1.2f;,双精度浮点数使用:double d = 1.2d; 在if语句中也可以这样写:if(c == 3.6f) - Martijn Courteaux
1
除了@bobah的答案之外,我建议查看Math.ulp()函数。 - aeracode
使用 BigDecimal 进行浮点数和双精度数的操作。请参考链接 - Aldis
9个回答

20

通常情况下,浮点数不应该像(a==b)那样进行比较,而应该像(Math.abs(a-b) < delta)这样进行比较,其中delta是一个很小的数字。

在十进制形式中具有固定位数的浮点值,在二进制形式中不一定具有固定位数。

为了更加明确,需要补充说明:

尽管严格使用==比较浮点数的意义非常小,但是相反地,严格使用<>比较则是一个有效的用例(例如 - 当特定值超过阈值时触发逻辑:(val > threshold) && panic();


2
推荐使用公差进行比较是不恰当的建议,因为它会以增加相等的错误报告为代价来减少不相等的错误报告,并且您无法知道这是否对您所知道的应用程序是可接受的。该应用程序可能更“关注”寻求不平等而不是寻求平等,或者可能有其他需要满足的规格要求。 - Eric Postpischil
2
@Eric - 在处理浮点数时,不存在身份或不平等的概念,只存在距离的概念。如果在我在答案中给出的公式中用>替换<,则可以得到一种比较浮点数不平等性的距离标准。对于大多数实际应用程序而言,浮点数在计算机内存中的位表示的身份是无关紧要的。 - bobah
1
你正在研究一个阻尼振荡器,并想区分欠阻尼、过阻尼和临界阻尼。这需要进行严格的测试,不能容忍任何误差。允许有误差会导致对负数取平方根。然而,尽管有这个例子,你的要求是一个伪命题。建议不要使用误差容限并不意味着要进行精确相等的比较,因为还有其他选择。例如,一种可能性是完全避免使用比较;只报告可用的最佳结果,而不试图将其强制转换为量化结果。 - Eric Postpischil
4
不考虑任何示例,向人们建议使用公差进行比较存在一个根本性问题。这会增加错误报告的数量,并且由于你不知道应用程序,因此无法确定这是否可接受或成为问题。 - Eric Postpischil
3
“准确比较”是一个毫无意义的术语,它无法量化。我认为我对IEEE754很了解,我给出的答案精确地回答了主题问题,简洁明了。相反,你的评论太笼统了,几乎成为了离题。 - bobah
显示剩余6条评论

7
如果您对固定精度数字感兴趣,应该使用类似于 BigDecimal 的固定精度类型,而不是本质上是近似的(虽然高精度)类型 float。在Stack Overflow上有许多类似的问题,涉及多种编程语言,详细阐述了这一点。

6

我认为这与Java无关,它会在任何IEEE 754浮点数上发生。这是由于浮点表示的性质所致。使用IEEE 754格式的任何语言都会遇到同样的问题。

正如David所建议的那样,您应该使用java.lang.Math类的abs方法来获取绝对值(去除正负号)。

您可以阅读此内容:http://en.wikipedia.org/wiki/IEEE_754_revision,同时一本好的数值方法教材也会充分解决这个问题。

public static void main(String[] args) {
    float a = 1.2f;
    float b = 3.0f;
    float c = a * b;
        final float PRECISION_LEVEL = 0.001f;
    if(Math.abs(c - 3.6f) < PRECISION_LEVEL) {
        System.out.println("c is 3.6");
    } else {
        System.out.println("c is not 3.6");
    }
}

3
我在单元测试中使用以下代码来比较两个不同计算的结果是否相同,除去浮点数计算误差。
它的原理是查看浮点数的二进制表示。大部分复杂性是由于浮点数的符号不是二进制补码造成的。在进行补偿后,基本上只需要进行简单的减法运算即可得到ULP(在下面的注释中解释)的差异。
/**
 * Compare two floating points for equality within a margin of error.
 * 
 * This can be used to compensate for inequality caused by accumulated
 * floating point math errors.
 * 
 * The error margin is specified in ULPs (units of least precision).
 * A one-ULP difference means there are no representable floats in between.
 * E.g. 0f and 1.4e-45f are one ULP apart. So are -6.1340704f and -6.13407f.
 * Depending on the number of calculations involved, typically a margin of
 * 1-5 ULPs should be enough.
 * 
 * @param expected The expected value.
 * @param actual The actual value.
 * @param maxUlps The maximum difference in ULPs.
 * @return Whether they are equal or not.
 */
public static boolean compareFloatEquals(float expected, float actual, int maxUlps) {
    int expectedBits = Float.floatToIntBits(expected) < 0 ? 0x80000000 - Float.floatToIntBits(expected) : Float.floatToIntBits(expected);
    int actualBits = Float.floatToIntBits(actual) < 0 ? 0x80000000 - Float.floatToIntBits(actual) : Float.floatToIntBits(actual);
    int difference = expectedBits > actualBits ? expectedBits - actualBits : actualBits - expectedBits;

    return !Float.isNaN(expected) && !Float.isNaN(actual) && difference <= maxUlps;
}

这里是一个双精度浮点数 double 的版本:
/**
 * Compare two double precision floats for equality within a margin of error.
 * 
 * @param expected The expected value.
 * @param actual The actual value.
 * @param maxUlps The maximum difference in ULPs.
 * @return Whether they are equal or not.
 * @see Utils#compareFloatEquals(float, float, int)
 */
public static boolean compareDoubleEquals(double expected, double actual, long maxUlps) {
    long expectedBits = Double.doubleToLongBits(expected) < 0 ? 0x8000000000000000L - Double.doubleToLongBits(expected) : Double.doubleToLongBits(expected);
    long actualBits = Double.doubleToLongBits(actual) < 0 ? 0x8000000000000000L - Double.doubleToLongBits(actual) : Double.doubleToLongBits(actual);
    long difference = expectedBits > actualBits ? expectedBits - actualBits : actualBits - expectedBits;

    return !Double.isNaN(expected) && !Double.isNaN(actual) && difference <= maxUlps;
}

1
您还可以考虑使用Float.floatToRawIntBits(),在方法开始时检查NaN。实际上,floatToIntBits()仅检查结果是否为NaN,并将其替换为预定义的整数值0x7fc00000。这样做的主要原因是floatToIntBits()实际上调用了floatToRawIntBits(),使其执行速度变慢。另一种方法是检查转换后的位是否为0x7fc00000,但您不需要进行两个检查。 - Netherwire

2

像其他人写的一样:

使用以下方法比较浮点数:if (Math.abs(a - b) < delta)

您可以编写一个好用的方法来执行此操作:

public static int compareFloats(float f1, float f2, float delta)
{
    if (Math.abs(f1 - f2) < delta)
    {
         return 0;
    } else
    {
        if (f1 < f2)
        {
            return -1;
        } else {
            return 1;
        }
    }
}

/**
 * Uses <code>0.001f</code> for delta.
 */
public static int compareFloats(float f1, float f2)
{
     return compareFloats(f1, f2, 0.001f);
}

所以,您可以像这样使用它:
if (compareFloats(a * b, 3.6f) == 0)
{
    System.out.println("They are equal");
}
else
{
    System.out.println("They aren't equal");
}

2

有一个用于比较双精度浮点数的Apache类:org.apache.commons.math3.util.Precision

它包含一些有趣的常量:SAFE_MINEPSILON,在执行算术运算时它们是可能的最大偏差。

它还提供了必要的方法来比较、相等或舍入双精度浮点数。


2
这是所有浮点数表示的一个弱点,因为在十进制系统中看似有固定小数位的一些数字,在二进制系统中实际上有无限多的小数位。因此,你认为的1.2实际上是类似于1.199999999997的东西,因为在二进制表示时必须在某个位置截断小数位,从而失去了一些精度。然后将其乘以3实际上得到的是3.5999999...。 http://docs.python.org/py3k/tutorial/floatingpoint.html <- 这可能会更好地解释它(即使它是针对Python的,但这是浮点数表示的常见问题)。

1
+1 - 所有 有限精度浮点数系统都存在这个问题。无论您选择什么基数,都无法准确表示某些有理数。 - Stephen C

0
四舍五入是一个不好的主意。使用 BigDecimal 并根据需要设置其精度。例如:
public static void main(String... args) {
    float a = 1.2f;
    float b = 3.0f;
    float c = a * b;
    BigDecimal a2 = BigDecimal.valueOf(a);
    BigDecimal b2 = BigDecimal.valueOf(b);
    BigDecimal c2 = a2.multiply(b2);
    BigDecimal a3 = a2.setScale(2, RoundingMode.HALF_UP);
    BigDecimal b3 = b2.setScale(2, RoundingMode.HALF_UP);
    BigDecimal c3 = a3.multiply(b3);
    BigDecimal c4 = a3.multiply(b3).setScale(2, RoundingMode.HALF_UP);

    System.out.println(c); // 3.6000001
    System.out.println(c2); // 3.60000014305114740
    System.out.println(c3); // 3.6000
    System.out.println(c == 3.6f); // false
    System.out.println(Float.compare(c, 3.6f) == 0); // false
    System.out.println(c2.compareTo(BigDecimal.valueOf(3.6f)) == 0); // false
    System.out.println(c3.compareTo(BigDecimal.valueOf(3.6f)) == 0); // false
    System.out.println(c3.compareTo(BigDecimal.valueOf(3.6f).setScale(2, RoundingMode.HALF_UP)) == 0); // true
    System.out.println(c3.compareTo(BigDecimal.valueOf(3.6f).setScale(9, RoundingMode.HALF_UP)) == 0); // false
    System.out.println(c4.compareTo(BigDecimal.valueOf(3.6f).setScale(2, RoundingMode.HALF_UP)) == 0); // true
}

-1

要比较两个浮点数 f1f2,精度为 #.###,我认为你需要这样做:

((int) (f1 * 1000 + 0.5)) == ((int) (f2 * 1000 + 0.5))

f1 * 10003.14159265... 提升至 3141.59265+ 0.5 得到 3142.09265,然后使用 (int) 去掉小数部分,得到 3142。也就是说,它包括3个小数位并正确地四舍五入最后一位数字。


使用一个小数精度值进行比较更好:考虑一下如果 f1 == 3.1414999999999f2 == 3.1415000000001 会发生什么。 - Mark Dickinson
我靠,我以为我搞定了 :-) 当然。我同意你的观点。使用一个 epsilon 进行比较更好。但是它能准确地比较两个浮点数的前三位小数吗? - aioobe

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