仅 Java 8 的数字格式舍入问题

2022-09-03 05:54:35

有人可以向我解释为什么下面的代码:

public class Test {
    public static void main(String... args) {
        round(6.2088, 3);
        round(6.2089, 3);
    }

    private static void round(Double num, int numDecimal) {
        System.out.println("BigDecimal: " + new BigDecimal(num).toString());

        // Use Locale.ENGLISH for '.' as decimal separator
        NumberFormat nf = NumberFormat.getInstance(Locale.ENGLISH);
        nf.setGroupingUsed(false);
        nf.setMaximumFractionDigits(numDecimal);
        nf.setRoundingMode(RoundingMode.HALF_UP);

        if(Math.abs(num) - Math.abs(num.intValue()) != 0){
            nf.setMinimumFractionDigits(numDecimal);
        }

        System.out.println("Formatted: " + nf.format(num));
    }
}

给出以下输出?

[me@localhost trunk]$ java Test
BigDecimal: 6.208800000000000096633812063373625278472900390625
Formatted: 6.209
BigDecimal: 6.208899999999999863575794734060764312744140625
Formatted: 6.208

如果您没有看到它:“6.2089”舍入为3位数字表示输出“6.208”,而“6.2088”表示“6.209”作为输出。少即是多?

使用Java 5,6或7时,结果很好,但是这个Java 8给了我这个奇怪的输出。Java 版本:

[me@localhost trunk]$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) Server VM (build 25.5-b02, mixed mode)

编辑:这是Java 7的输出:

[me@localhost trunk]$ java Test
BigDecimal: 6.208800000000000096633812063373625278472900390625
Formatted: 6.209
BigDecimal: 6.208899999999999863575794734060764312744140625
Formatted: 6.209

Java 7 版本:

[me@localhost trunk]$ java -version
java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Server VM (build 24.51-b03, mixed mode)

答案 1

我可以将此问题追溯到第522行。java.text.DigitList

情况是,它认为十进制数字已经舍入(与等效表示相比是正确的),并决定不再四舍五入。问题在于,只有在舍入产生的数字是 的情况下,这个决定才有意义,而不是当它大于 。请注意 的代码如何正确区分 和 大小写。6.0289BigDecimal6.208899…55HALF_DOWNdigit=='5'digit>'5'

显然,这是一个错误,而且是一个奇怪的错误,因为做类似事情的代码(只是针对另一个方向)就在破碎的代码下面。

        case HALF_UP:
            if (digits[maximumDigits] >= '5') {
                // We should not round up if the rounding digits position is
                // exactly the last index and if digits were already rounded.
                if ((maximumDigits == (count - 1)) &&
                    (alreadyRounded))
                    return false;

                // Value was exactly at or was above tie. We must round up.
                return true;
            }
            break;
        case HALF_DOWN:
            if (digits[maximumDigits] > '5') {
                return true;
            } else if (digits[maximumDigits] == '5' ) {
                if (maximumDigits == (count - 1)) {
                    // The rounding position is exactly the last index.
                    if (allDecimalDigits || alreadyRounded)
                        /* FloatingDecimal rounded up (value was below tie),
                         * or provided the exact list of digits (value was
                         * an exact tie). We should not round up, following
                         * the HALF_DOWN rounding rule.
                         */
                        return false;
                    else
                        // Value was above the tie, we must round up.
                        return true;
                }

                // We must round up if it gives a non null digit after '5'.
                for (int i=maximumDigits+1; i<count; ++i) {
                    if (digits[i] != '0') {
                        return true;
                    }
                }
            }
            break;

这种情况不会发生在另一个数字上的原因是这不是四舍五入的结果(再次,与输出相比)。因此,在这种情况下,它将四舍五入。6.2088BigDecimal6.208800…


答案 2

Oracle 修复了 Java 8 update 40 中的此 bug

非官方运行时修补程序可用于早期版本

多亏了Holger答案中的研究,我能够开发一个运行时补丁,我的雇主已经根据GPLv2许可证的条款免费发布了它,并带有Classpath Exception1(与OpenJDK源代码相同)。

补丁项目和源代码托管在GitHub上,其中包含有关此错误的更多详细信息以及指向可下载二进制文件的链接。该修补程序不会对磁盘上已安装的 Java 文件进行任何修改,并且它应该可以安全地用于 Oracle Java >= 6 的所有版本,并且至少通过版本 8(包括固定版本)。

当修补程序检测到表明存在 bug 的字节码签名时,它会用修改后的实现替换开关案例:HALF_UP

if (digits[maximumDigits] > '5') {
    return true;
} else if (digits[maximumDigits] == '5') {
    return maximumDigits != (count - 1)
        || allDecimalDigits
        || !alreadyRounded;
}
// else
return false; // in original switch(), was: break;

1 我不是律师,但我的理解是,GPLv2 w/ CPE 允许以二进制形式进行商业使用,而无需 GPL 应用于组合工作。


推荐