为什么toString无法在不可变的BigDecimal上产生正确的值?

import java.math.BigDecimal; import java.math.RoundingMode; public class BigDecimalTest { public static void main (String[] args) { // 4.88...e+888 (1817 digits) BigDecimal x = new BigDecimal("4.+888"); BigDecimal y = new BigDecimal("7.11510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537904743732558993126e+302"); BigDecimal z = x.divide(y, 0, RoundingMode.HALF_UP); System.out.println("x: " + x.toString()); System.out.println(); System.out.println("y: " + y.toString()); System.out.println(); System.out.println("z: " + z.toString()); } } 

 >javac BigDecimalTest.java 

执行

  >java BigDecimalTest 

产量

 x: . y: 711510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537.904743732558993126 z: 

输出中的z.toString()的值是正确的

 4.883242e+888 / 7.115109e+302 = 6.863200e+585 

就像y.toString()的值y.toString() ,但是注意为x.toString()给出的值是完全错误的。

为什么是这样?

奇怪的是,如果划分结果的比例(即所需的小数位)改变了

 BigDecimal z = x.divide(y, 3, RoundingMode.HALF_UP); 

那么x.toString()将为x生成正确的值。

或者,如果操作数交换

 BigDecimal z = y.divide(x, 0, RoundingMode.HALF_UP); 

那么x.toString()也会产生正确的值。

或者,如果x的指数从e+888改变成例如e+878x.toString()将是正确的。

或者,如果在divide操作之上添加另一个x.toString()调用,则x.toString()调用将产生正确的值!

在我testing这台机器上,Windows 7 64位,使用Java 7和8,都是32位和64位版本的行为是相同的,但在线testing在https://ideone.com/产生不同的结果为Java 7和java 8。

使用java 7, x的值是正确的: http : //ideone.com/P1sXQQ ,但使用java 8它的值是不正确的: http : //ideone.com/OMAq7a 。

此外,这种行为并不是唯一的x特定值,因为在将其作为第一个操作数传递给divide操作之后,对其他具有超过约1500位数字的BigDecimal调用toString也会产生不正确的值。

这是什么解释?

divide操作似乎正在改变后续的toString调用在其操作数上产生的值。

这是否发生在您的平台上?

编辑:

这个问题似乎只与java 8运行库有关,因为使用java 7编译的上述程序在使用java 7运行时执行时会生成正确的输出,但在使用java 8运行时执行时会产生错误的输出。

编辑:

我已经testing了早期的访问jre1.8.0_60 ,并且没有出现该错误,根据Marco13的回答,它在build 51中得到了修复。Oracle JDK 8产品二进制文件仅在更新40时才有效,但可能需要一段时间固定版本被广泛使用。

追查这种奇怪行为的原因并不难。

divide电话去

 public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) { return divide(divisor, scale, roundingMode.oldMode); } 

这在内部根据四舍五入模式代表另一种divide方法:

 public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) { if (roundingMode < ROUND_UP || roundingMode > ROUND_UNNECESSARY) throw new IllegalArgumentException("Invalid rounding mode"); if (this.intCompact != INFLATED) { if ((divisor.intCompact != INFLATED)) { return divide(this.intCompact, this.scale, divisor.intCompact, divisor.scale, scale, roundingMode); } else { return divide(this.intCompact, this.scale, divisor.intVal, divisor.scale, scale, roundingMode); } } else { if ((divisor.intCompact != INFLATED)) { return divide(this.intVal, this.scale, divisor.intCompact, divisor.scale, scale, roundingMode); } else { return divide(this.intVal, this.scale, divisor.intVal, divisor.scale, scale, roundingMode); } } } 

在这种情况下,最后的呼叫适用。 请注意, intVal (存储在BigDecimalBigInteger )作为第一个参数直接传递给此方法:

 private static BigDecimal divide(BigInteger dividend, int dividendScale, BigInteger divisor, int divisorScale, int scale, int roundingMode) { if (checkScale(dividend,(long)scale + divisorScale) > dividendScale) { int newScale = scale + divisorScale; int raise = newScale - dividendScale; BigInteger scaledDividend = bigMultiplyPowerTen(dividend, raise); return divideAndRound(scaledDividend, divisor, scale, roundingMode, scale); } else { int newScale = checkScale(divisor,(long)dividendScale - scale); int raise = newScale - divisorScale; BigInteger scaledDivisor = bigMultiplyPowerTen(divisor, raise); return divideAndRound(dividend, scaledDivisor, scale, roundingMode, scale); } } 

最后, 第二个 divideAndRound的path在这里被采用,再次传递dividend (这是原始BigDecimal ),结束于这个代码:

 private static BigDecimal divideAndRound(BigInteger bdividend, BigInteger bdivisor, int scale, int roundingMode, int preferredScale) { boolean isRemainderZero; // record remainder is zero or not int qsign; // quotient sign // Descend into mutables for faster remainder checks MutableBigInteger mdividend = new MutableBigInteger(bdividend.mag); MutableBigInteger mq = new MutableBigInteger(); MutableBigInteger mdivisor = new MutableBigInteger(bdivisor.mag); MutableBigInteger mr = mdividend.divide(mdivisor, mq); ... 

这就是错误引入的地方: mdivididend是一个可变的 BigInteger ,它被创build为BigIntegermag数组上的可变视图, BigInteger存储在原始调用的BigDecimal x 。 该分区修改mag域,从而修改(现在不是那么一成不变的) BigDecimal

这显然是执行其中一种divide方法时的一个缺陷。 我已经开始跟踪OpenJDK的变化集,但还没有发现明确的罪魁祸首。 ( 编辑:见下面的更新

(注意:在进行除法之前调用x.toString()并不能真正避免 ,而只能隐藏这个bug:它会导致内部创build正确状态的stringcaching,打印出正确的值,但是内部状态仍然是错的 – 至less可以这么说…)


更新 :确认@MikeM说的是什么:bug已经列在openjdk的bug列表上 ,并已经在JDK8 Build 51

更新 :对Mike和exex zian挖出错误报告的荣誉。 根据那里的讨论,这个错误是在这个变更集中引入的。 (不可否认,在撇清这些变化的同时,我也认为这是一个热门人选,但不能相信这是四年前引入的,到现在为止一直未被注意到)