NumberFormat舍入问题仅限Java 8

时间:2014-06-26 09:02:54

标签: java java-8

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

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)

3 个答案:

答案 0 :(得分:19)

我可以将这个问题追溯到java.text.DigitList课程522。

情况是它认为小数位数6.0289已经四舍五入(与等效的BigDecimal表示6.208899…比较时这是正确的)并且决定不再向上舍入。问题是这个决定只有在四舍五入产生的数字为5的情况下才有意义,而不是在大于5的情况下。请注意HALF_DOWN的代码如何正确区分digit=='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.2088不是四舍五入的结果(再次,与BigDecimal输出6.208800…相比)。所以在这种情况下,它会向上舍入。

答案 1 :(得分:3)

Oracle修复了Java 8 update 40

中的这个错误

非官方运行时补丁可用于早期版本

感谢Holger's answer的研究,我能够开发一个运行时补丁,我的雇主已经根据GPLv2许可条款免费发布了它,并且有类路径异常 1 (与OpenJDK源代码相同)。

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

当补丁检测到表明存在错误的字节码签名时,它会用修改后的实现取代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合并后的工作。

答案 2 :(得分:2)

通过代码追踪到达DigitList.set

final void set(boolean isNegative, double source, int maximumDigits, boolean fixedPoint) {

    FloatingDecimal.BinaryToASCIIConverter fdConverter  = FloatingDecimal.getBinaryToASCIIConverter(source);
    boolean hasBeenRoundedUp = fdConverter.digitsRoundedUp();

我对此错误进行了更简单的测试

import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;

public class Test {
    public static void main(String... args) {
        for (int i = 0; i < 100; i++)
            test(i / 100.0);
    }

    private static void test(double num) {
        NumberFormat nf = NumberFormat.getInstance(Locale.ENGLISH);
        nf.setMaximumFractionDigits(1);
        String round1 = nf.format(num);

        NumberFormat nf2 = NumberFormat.getInstance(Locale.ENGLISH);
        nf2.setMaximumFractionDigits(1);
        nf2.setRoundingMode(RoundingMode.HALF_UP);
        String round2 = nf2.format(num);
        if (!round1.equals(round2))
            System.out.printf("%s, formatted with HALF_UP was %s but should be %s%n", num, round2, round1);
    }
}

打印

0.06, formatted with HALF_UP was 0 but should be 0.1
0.09, formatted with HALF_UP was 0 but should be 0.1
0.18, formatted with HALF_UP was 0.1 but should be 0.2
0.25, formatted with HALF_UP was 0.3 but should be 0.2
0.29, formatted with HALF_UP was 0.2 but should be 0.3
0.36, formatted with HALF_UP was 0.3 but should be 0.4
0.37, formatted with HALF_UP was 0.3 but should be 0.4
0.47, formatted with HALF_UP was 0.4 but should be 0.5
0.48, formatted with HALF_UP was 0.4 but should be 0.5
0.49, formatted with HALF_UP was 0.4 but should be 0.5
0.57, formatted with HALF_UP was 0.5 but should be 0.6
0.58, formatted with HALF_UP was 0.5 but should be 0.6
0.59, formatted with HALF_UP was 0.5 but should be 0.6
0.69, formatted with HALF_UP was 0.6 but should be 0.7
0.86, formatted with HALF_UP was 0.8 but should be 0.9
0.87, formatted with HALF_UP was 0.8 but should be 0.9
0.96, formatted with HALF_UP was 0.9 but should be 1
0.97, formatted with HALF_UP was 0.9 but should be 1
0.98, formatted with HALF_UP was 0.9 but should be 1
0.99, formatted with HALF_UP was 0.9 but should be 1

在不正确的情况下hasBeenRoundedUp为真,这可以防止任何进一步的舍入。请注意,如果您放弃设置舍入,则它具有正确舍入的默认路径。

我不会使用NumberFormat。使用起来非常缓慢和复杂。

import java.math.BigDecimal;

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) {
        BigDecimal bd = new BigDecimal(num);
        BigDecimal bd2 = BigDecimal.valueOf(num);
        System.out.println("new BigDecimal: " + bd);
        System.out.println("BigDecimal.valueOf: " + bd2);
        System.out.printf("%." + numDecimal + "f%n", num);
        System.out.printf("%." + numDecimal + "f%n", bd);
        System.out.printf("%." + numDecimal + "f%n", bd2);
        System.out.printf("%f%n", round3(num));
        System.out.printf("%s%n", round3(num));
        System.out.printf("%f%n", bd.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
        System.out.printf("%s%n", bd.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
        System.out.printf("%f%n", bd2.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
        System.out.printf("%s%n", bd2.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
    }

    private static double round3(double num) {
        final double factor = 1e3;
        return Math.round(num * factor) / factor;
    }
}

使用Java 8进行打印。

new BigDecimal: 6.208800000000000096633812063373625278472900390625
BigDecimal.valueOf: 6.2088
6.209
6.209
6.209
6.209000
6.209
6.209000
6.209
6.209000
6.209
new BigDecimal: 6.208899999999999863575794734060764312744140625
BigDecimal.valueOf: 6.2089
6.209
6.209
6.209
6.209000
6.209
6.209000
6.209
6.209000
6.209