Java 7和Java 8之间的舍入不一致是一个错误吗?

Asa*_*aph 36 java rounding

我发现DecimalFormatjava 7和java 8之间的类舍入不一致.这是我的测试用例:

DecimalFormatTest.java

import java.text.DecimalFormat;

public class DecimalFormatTest {
    public static void main(String[] args) {
        DecimalFormat format = (DecimalFormat) DecimalFormat.getInstance();
        format.setDecimalSeparatorAlwaysShown(true);
        format.setMinimumFractionDigits(1);
        format.setMaximumFractionDigits(1);
        System.out.println(format.format(83.65));
    }
}
Run Code Online (Sandbox Code Playgroud)

在Java(TM)SE运行时环境(版本1.7.0_51-b13)中,输出为:

83.6
Run Code Online (Sandbox Code Playgroud)

在Java(TM)SE运行时环境(版本1.8.0-b132)中,输出为:

83.7
Run Code Online (Sandbox Code Playgroud)

这是一个回归错误吗?或者随着Java 8的发布,舍入规则发生了变化?

mel*_*okb 23

看起来这是JDK 7中一个长期存在的错误,最终得到修复.参见例如:

有一个计划草案,为JDK 8提供以下建议,解释了这个问题:

-------------------------------------------------- -------------------区域:核心库/ java.text

概要:JDK7的错误舍入行为已得到修复.当值非常接近准确位于格式化模式中指定的舍入位置的平局时,NumberFormat/DecimalFormat format()方法的舍入行为已更改.

不相容的性质:行为

描述:使用NumberFormat/DecimalFormat类时,以前的JDK版本的舍入行为在某些极端情况下是错误的.当使用非常接近平局的值调用format()方法时会发生这种错误行为,而使用的NumberFormat/DecimalFormat实例的模式指定的舍入位置恰好位于平局的位置.在这种情况下,发生了错误的双舍入或错误的非舍入行为.

例如,在使用默认推荐的NumberFormatFormatAPI表单时:NumberFormat nf = java.text.NumberFormat.getInstance()后面跟着nf.format(0.8055d),值0.8055d记录在计算机中, 0.80549999999999999378275106209912337362766265869140625因为此值无法以二进制格式精确表示.这里默认的舍入规则是"半连",并呼吁在JDK7格式()的结果是"0.806"一个错误的输出,而正确的结果是"0.805",因为由计算机记录在存储器值"下方"领带.

对于可能由程序员选择的任何模式(非默认模式)定义的所有舍入位置,也实现了这种新行为.

RFE 7131459


  • 我不确定你为什么抱怨JDK如何工作 - 我只是转发我发现的信息.无论如何,OP获得83.7的原因是因为计算机在技术上将数字四舍五入为"83.650000000000005684341886080801486968994140625",这正确地向上舍入到83.7.基本上,如果你想使用类似金钱的值,那么不要使用浮点类型. (5认同)
  • 希望999999他们使用BigDecimals赚钱,而不是双打或花车. (5认同)
  • @ 2rs2ts因为83.65不是真的83.65.新行为是正确的. (4认同)
  • @rupps - 任何使用浮点的财务程序在*此更改之前已被破坏*.我担心,如果这样的程序存在,那么这种回归的责任应该完全落在程序员身上.我记得在1970年代后期被告知不要在我的CS学位上用FP换钱.它在1960年代甚至是众所周知的...... (4认同)
  • 我想知道这会如何影响用java编写的百万金融程序!! (3认同)
  • 不完全是程序员.在程序员,代码审查员,系统测试人员以及其他参与编程标准实施的人员身上. (2认同)
  • 通常,有人写一个`Money`类,它基本上是一个`BigDecimal`的包装器,可能有一个构造函数来检查有2个小数位.它可能有一些格式化方法,或者可能在其他地方定义.它也可能存储货币,以及实际的"BigDecimal"; 并进行一些验证,以确保您不会将USD添加到GBP.有些组织可能更喜欢他们的`Money`类是一个'long`的包装器,代表一些美分.但是,是的,在任何地方都不是"漂浮"或"双重". (2认同)

Wil*_*ice 15

正如在这个问题的其他答案中所提到的,JDK 8对问题JDK-7131459中的舍入进行了有意的更改:DecimalFormat在接近平局时产生错误的format()结果.DecimalFormat

但是,这些更改引入了一个真正的错误,JDK-8039915:错误的NumberFormat.format()HALF_UP在最后一个数字正好在舍入位置大于5时进行舍入.例如:

99.9989 -> 100.00
99.9990 ->  99.99
Run Code Online (Sandbox Code Playgroud)

简而言之,这表明一个更高的数字向下舍入,而更低的数字向上舍入的情况:(x <= y) != (round(x) <= round(y)).它似乎只影响HALF_UP舍入模式,这是在小学运算类中教授的一种舍入:总是从零开始0.5舍入.

此问题存在于Java 8的Oracle和OpenJDK版本中,并且更新了8u5,8u11,8u20,8u25和8u31.

Oracle在Java 8 update 40中修复了这个错误

早期版本可以使用非官方的运行时补丁

感谢Holger这个相关问题的答案中的研究,我能够开发一个运行时补丁,我的雇主已经根据GPLv2许可条款释放了它,并带有Classpath异常1(与OpenJDK源代码相同).

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

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