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

Why does toString fail to produce the correct value on an immutable 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.8832420563130171734733855852454330503023811919919497272520875234748556667894678622576481754268427107559208829679871295885797242917923401597269406065677191699322289667695163278484184288979073748578074654323955355081326227413484377691676742424283166095829482224974429868654315166151274143385980609237680132582337344627820946638217515894542788180511625488217105374918015830882194114839900966043221545533114607439892553114356192220778082796185122942407317178325055570254731781136589172583464356709469398354084238614163644229733602505332671951571644165960364672255033809137641462904872690406789293887232669588297154237004709334039097468524122773548736567569610163195984254280720739773383424940292419418795538600322135358425131164741597944425501875163782825762694824406500718290697914964822219714335320528259344719705157913218736206355811213275685167080292570345461898557739737178480700932922510537942188898832900701474604169230215866582286672118698263686941093382945779882215421032414999405126831495224267159359035083987132591639397950272617333366716471522059176764287433877865132652162238979110053714139119937420203828830308427979430335027147927304099711225033972679405835031675622271744826476172494554124259735452592820489036311645033355738586053207859638698142614469753279404304130088308403735928520706825401977138623732336487326694527108332032932321484204820451539099031068139840323111890984119271864483907126875945501867099986131423579718697889448836497435592993168391953327829695391643033262276364164246663414855044991442223872210174626308430613254236633497864858897399515832571171741522071020097519091890029843359547212185712419638040776450730043492270253991396124987467648536016180816769990203447616590740625203442076233929983869509074724986395815800482885710533831896927860285993286232937744729344906236207008084e+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: 625054983208066198204593354911415430438704792574969565088267203004781525349051886368978966454635866976757873019902352587338204709349419540445048397640668053751325307746498089964597558898932143981799355575346628545040975710892600034453462303030824526026617372479672702318775234126736309035340551798242305697053918011236108116969184203450147688710548806249178948798950602635292084669950732365353235782823866975230624679863759260425959459791169573662813659882560711299260566798548341409068343765881208298932278254261294646140590112068258200980117045324292667804864432756961810725182370437206902961756578170730203574233660279475700447597108771501423828064891010088908598454793225469099307839235742968560582894084123332587841678908692453688646424002096420169762493752403209194120933311549724412343492102761719612412226021289199823441354383529928770138627744900421912301539068635884552971941408134.8856600179050611289788749333661467630922532694031193377751928459953017059824923573892149119923856234431388706196397956490750352971729842937634895018670939708354823574625828791536366736979476766589326086875409807351989786090090279478781367082883474934694924763036804348502963946884054479650783337788950079302927905246137931881022596647890564269534539014810606033753362254652128419763750928651303475678198850650473651453073743837739070377816899469866500215337149978217017797004675976721899561358322045967266798653940112240121024238988798224822218203993329849451071671755903125554170025962201010130308257571374613023572917101445758904604655642902352167479118496542289087726701938867138026569109982914825090572482443761923819950022043159771189713669219385693445567010592510898703998395859012610071144546558746041294923614800026040585757943037935297161564798258664422461809370948330482806766116607140637816031325356147998234497034752

y: 711510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537.904743732558993126

z: 6863200148645991450016700150728475158275817266239021182863526677885700921863906334312309256001619020949572592642200844420107346867400206096485382274175041601107978676753014927820457112641389679172479926134263590581506384223135957016211147412682886175625161361918270282067511320630977561140325469899962049739132122854543111824994613211802165652292305592183629295330885779837415870933600699791946039851356918600890315497940083093271504897016557099915008808164166772999720870505507779642391694002178573568389923682384862328430119487673749084566046514914589822168578412569408216619911686172

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

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

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+878 然后 x.toString() 将是正确的。

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

在我正在测试的机器上,Windows 7 64 位,使用 java 7 和 8(32 位和 64 位版本)的行为相同,但在 https://ideone.com/ 对 java 7 和 java 8 产生不同的结果。

使用java 7、正确给出x的值:http://ideone.com/P1sXQQ, but using java 8 its value is incorrect: http://ideone.com/OMAq7a.

此外,这种行为并不是 x 的这个特定值所独有的,因为在将其他 BigDecimals 作为第一个操作数传递给 divide 操作也会产生不正确的值。

这是怎么解释的?

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

您的平台上是否发生这种情况?

编辑:

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

编辑:

我已经使用早期访问 jre1.8.0_60 进行了测试,但该错误没有出现,根据 Marco13 的回答,它已在构建 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(这是存储在 BigDecimal 中的 BigInteger)作为第一个参数直接传递给此方法:

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

最后,second divideAndRound 的路径在这里,再次传递 dividend on(这是 intVal原来的 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 是一个 mutable BigInteger,它是作为 [=] 上的可变视图创建的BigInteger 的 25=] 数组存储在原始调用的 BigDecimal x 中。除法修改 mag 字段,因此,(现在不是那么不变的)BigDecimal.

的状态

这显然是 divide 方法之一的实现中的错误。我已经开始跟踪 OpenJDK 的变更集,但还没有发现真正的罪魁祸首。 (编辑:查看下面的更新

(旁注:在进行除法之前调用 x.toString() 并不能真正 避免 ,而只是 隐藏 这个错误: 它导致在内部创建正确状态的字符串缓存。打印了正确的值,但内部状态仍然是错误的——这至少可以说是令人担忧的...)


Update: To confirm what @MikeM said: Bug has been listed on openjdk bug list and it has been resolved in JDK8 Build 51

Update : Kudos to Mike and exex zian for digging out the bug reports. According to the discussion there, the bug was introduced with this changeset. (Admittedly, while skimming through the changes, I also considered this as a hot candidate, but could not believe that this was introduced four years ago and remained unnoticed until now...)