在Scala和Java中,RoundingMode.HALF_UP的区别

4
在Scala中,我正在尝试使用HALF_UP舍入模式将以下数字四舍五入到小数点后三位精度。 8409.3555
不幸的是,Scala返回8409.356,而Java返回8409.355。
Scala代码:
def round(d: Double): Double = {
    BigDecimal.apply(d).setScale(3, RoundingMode.HALF_UP).doubleValue()
}

Java 代码:

private static Double round(Double d) {
    BigDecimal bd = new BigDecimal(d);
    bd = bd.setScale(3, RoundingMode.HALF_UP);
    return bd.doubleValue();
}

有没有已知的问题?


你是如何生成 8409.3555 并传递给 round 函数的?这两次都是在 Scala 中吗?你尝试过从 Scala 调用 Java 的 round 函数,反之亦然吗? - The Archetypal Paul
我正在生成一个双精度浮点数 "Double d = 8409.3555;"。 - Sumon Rahman
我选择编写一个Java实用类,并在Scala代码中使用它以保持一致性。 - Sumon Rahman
2个回答

3
一般情况下,不建议直接将字面量的双精度数转换为BigDecimal,因为可能会受到浮点数舍入误差的影响。在scaladoc中也有一个警告:

从Double或Float创建BigDecimal时,必须小心,因为Double和Float的二进制小数表示不容易转换为十进制表示。

我们可以通过java.math.BigDecimal看到这种情况发生:
scala> new java.math.BigDecimal(8409.3555)
res1: java.math.BigDecimal = 8409.355499999999665305949747562408447265625

当你试图将那个数字(一半)向上舍入时,它现在是8409.355。scala.math.BigDecimal.apply使用MathContext立即舍入传递给applyDouble,以便生成的BigDecimal具有与您传递的文字Double相同的值的增加机会。在第一个代码片段中,实际调用的是:MathContext
scala> scala.math.BigDecimal(8409.3555)(java.math.MathContext.DECIMAL128)
res10: scala.math.BigDecimal = 8409.3555

最好将这些字面量表示为字符串,以避免存储Double时出现任何舍入误差。例如:
scala> scala.math.BigDecimal("8409.3555")
res17: scala.math.BigDecimal = 8409.3555

scala> new java.math.BigDecimal("8409.3555")
res18: java.math.BigDecimal = 8409.3555

如果你必须从Double转换,我建议使用scala.math.BigDecimal.apply来完成。

0

我也能重现这个问题。

def roundWithScala(d: Double): Double = {
    BigDecimal(d).setScale(3, RoundingMode.HALF_UP).doubleValue()
}

def roundWithJava(d: Double): Double = {
    val bd = new java.math.BigDecimal(d).setScale(3, java.math.RoundingMode.HALF_UP)
    return bd.doubleValue()
}

println(roundWithScala(8407.3555)) //8407.356
println(roundWithJava(8407.3555)) //8407.355

println(roundWithScala(8409.3555)) //8409.356
println(roundWithJava(8409.3555)) //8409.355

println(roundWithScala(8409.4555)) //8409.456
println(roundWithJava(8409.4555)) //8409.456

我注意到的第一件事是Scala使用不同于Java的MathContext

我尝试在两者中使用相同的MathContext.UNLIMITED,但没有改变任何东西。

然后我注意到,在Scala的BigDecimal中,Java的BigDecimal是这样构建的:

new BigDecimal(new BigDec(java.lang.Double.toString(d), mc), mc)

我尝试在Java中使用它:

 val bd = new java.math.BigDecimal(java.lang.Double.toString(d), java.math.MathContext.UNLIMITED).setScale(3, java.math.RoundingMode.HALF_UP)

我得到了相同的结果 (8409.356)。

基本上,你得到不同结果的原因是 double 在 Scala 的构造函数中被转换为字符串。

也许在你的情况下可以使用 Java 的 BigDecimal(就像我在 roundWithJava 中所做的那样)?


1
我正在将基于JAVA的ETL代码重写为基于scala/spark的ETL。当我比较旧流程结果和新流程时,我注意到了这个舍入误差。我本来想使用scala库,但现在我使用Java库以确保结果一致。谢谢。 - Sumon Rahman

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