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

仅限Java 8的数字格式舍入问题,java,java-8,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: " +

有人能给我解释一下为什么会出现以下代码:

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”。少就是多

当使用Java5、6或7时,结果很好,但是Java8给了我这个奇怪的输出。 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)

通过代码跟踪到DigitList.set

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

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

对于这个bug,我有一个更简单的测试

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);
    }
}

在不正确的情况下,
hasbeenRounded
为真,这将阻止进一步的四舍五入。请注意,如果放弃舍入设置,则它具有正确舍入的默认路径

我不会用数字格式。这是相当缓慢和复杂的使用

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;
    }
}
用Java8打印

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

我可以把这个问题追溯到class
java.text.DigitList
第522行

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

显然,这是一个bug,而且是一个奇怪的bug,因为用于正确执行类似操作(仅用于另一个方向)的代码就在错误代码的正下方

        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;
case HALF\u UP:
如果(位数[最大位数]>='5'){
//如果四舍五入数字的位置是正确的,我们不应该四舍五入
//如果数字已经四舍五入,则正好是最后一个索引。
如果((最大位数==(计数-1))&&
(已命名)
返回false;
//价值正好等于或高于平局。我们必须汇总。
返回true;
}
打破
下半格:
如果(位数[最大位数]>“5”){
返回true;
}else if(位数[最大位数]='5'){
如果(最大位数==(计数-1)){
//取整位置正好是最后一个索引。
if(所有小数位数| |已满)
/*浮动小数向上舍入(值低于平分),
*或提供准确的数字列表(值为
*一个精确的平局)。我们不应该围捕,跟随
*半舍五入规则。
*/
返回false;
其他的
//价值高于平局,我们必须收拢。
返回true;
}
//如果在“5”之后出现非空数字,则必须将其取整。
对于(int i=maximumDigits+1;iOracle在Java8更新40中修复了此错误
  • 跟踪OpenJDK
  • 追踪
  • 2015年3月3日,采用JDK
    1.8.0_40-b25的8u40公开发行()
早期版本提供了一个非官方的运行时补丁 由于年的研究,我能够开发一个运行时补丁,我的雇主根据GPLv2许可证的条款免费发布了它,其中包含类路径例外1(与OpenJDK源代码相同)

修补程序项目和源代码包含有关此错误的更多详细信息以及可下载二进制文件的链接。修补程序不对磁盘上已安装的Java文件进行任何修改,并且在Oracle Java>=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应用于组合工作的情况下以二进制形式进行商业使用。

真的,必须在我的工作中进行检查!在打印之前对BigDecimal进行四舍五入如何?使用其他带有字符串参数的BigDecimal构造函数。这只是关于java中双精度和浮点的限制,但字符串可以很好地工作。@ferrerverck你真的确定Java在
double
float
数据类型上有这样的限制吗!@PeterLawrey:我会在工作后测试它,因为它可能被用作解决方法,但它仍然不能解释数字格式的行为,是吗?你比我快38倍:-)但是,这里的错误不是假设
hasBeenRoundedUp
,而是这个特殊舍入模式的结果。我的答案中详细说明了…+1,用于观察它已经被修正了一半。万一有人想知道为什么他们真的改变了算法:Java 7对像e这样的值是错误的。g、
1234567890123.45949
。因此修复程序引入了一个新的错误…解决方法是使用
nf.format(new BigDecimal(num))
避免这两个错误。您不喜欢
nf.format(BigDecimal.valueOf(num))
@Peter Lawrey:
new BigDecimal(double)
BigDecimal.valueOf(double)
不一样。后者将执行隐式的舍入到双精度操作,这将重新引入
123456789013.45949
的双舍入问题。但是,如果您实际上是指
123456789013.4595
,则
valueOf
将是更好的选择。如果您想安全起见,请使用
bigdecimic.valueOf(String)
new BigDecimal(String)
对于使用
BigDecimal的值没有区别。valueOf(double)
而不是
new BigDecimal(double)
没有区别。Bu
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;