Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
110 views
in Technique[技术] by (71.8m points)

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

Compile

>javac BigDecimalTest.java

Execute

 >java BigDecimalTest

Output

x: 625054983208066198204593354911415430438704792574969565088267203004781525349051886368978966454635866976757873019902352587338204709349419540445048397640668053751325307746498089964597558898932143981799355575346628545040975710892600034453462303030824526026617372479672702318775234126736309035340551798242305697053918011236108116969184203450147688710548806249178948798950602635292084669950732365353235782823866975230624679863759260425959459791169573662813659882560711299260566798548341409068343765881208298932278254261294646140590112068258200980117045324292667804864432756961810725182370437206902961756578170730203574233660279475700447597108771501423828064891010088908598454793225469099307839235742968560582894084123332587841678908692453688646424002096420169762493752403209194120933311549724412343492102761719612412226021289199823441354383529928770138627744900421912301539068635884552971941408134.8856600179050611289788749333661467630922532694031193377751928459953017059824923573892149119923856234431388706196397956490750352971729842937634895018670939708354823574625828791536366736979476766589326086875409807351989786090090279478781367082883474934694924763036804348502963946884054479650783337788950079302927905246137931881022596647890564269534539014810606033753362254652128419763750928651303475678198850650473651453073743837739070377816899469866500215337149978217017797004675976721899561358322045967266798653940112240121024238988798224822218203993329849451071671755903125554170025962201010130308257571374613023572917101445758904604655642902352167479118496542289087726701938867138026569109982914825090572482443761923819950022043159771189713669219385693445567010592510898703998395859012610071144546558746041294923614800026040585757943037935297161564798258664422461809370948330482806766116607140637816031325356147998234497034752

y: 711510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537.904743732558993126

z: 6863200148645991450016700150728475158275817266239021182863526677885700921863906334312309256001619020949572592642200844420107346867400206096485382274175041601107978676753014927820457112641389679172479926134263590581506384223135957016211147412682886175625161361918270282067511320630977561140325469899962049739132122854543111824994613211802165652292305592183629295330885779837415870933600699791946039851356918600890315497940083093271504897016557099915008808164166772999720870505507779642391694002178573568389923682384862328430119487673749084566046514914589822168578412569408216619911686172

The value of z.toString() in the output is correct

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

as is the value of y.toString(), but notice that the value given for x.toString() is completely wrong.

Why is this?

Strangely, if the scale (i.e. desired decimal places) of the result of the division is changed

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

then x.toString() will produce the correct value for x.

Or, if the operands are swapped

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

then x.toString() will also then produce the correct value.

Or, if the exponent of x is changed from e+888 to e.g. e+878 then x.toString() will be correct.

Or, if another x.toString() call is added above the divide operation, then both x.toString() calls will produce the correct value!

On the machine I'm testing this, Windows 7 64 bit, the behaviour is the same using java 7 and 8, both 32bit and 64 bit versions, but testing online at https://ideone.com/ produces different results for java 7 and java 8.

Using java 7, the value of x is given correctly: http://ideone.com/P1sXQQ, but using java 8 its value is incorrect: http://ideone.com/OMAq7a.

Also, this behaviour is not unique to this particular value of x, as calling toString on other BigDecimals with more than about 1500 digits after passing them as the first operand to a divide operation will also produce incorrect values.

What is the explanation for this?

The divide operation seems to be mutating the value produced by subsequent toString calls on its operands.

Does this happen on your platform?

Edit:

The issue seems to be with the java 8 runtime only, as the above program compiled with java 7 produces correct output when executed with the java 7 runtime, but incorrect output when executed with the java 8 runtime.

Edit:

I've tested with the early access jre1.8.0_60 and the bug does not appear, and according to Marco13's answer it was fixed in build 51. The Oracle JDK 8 product binaries are only at update 40 though so it may be some time before the fixed versions are widely used.

question from:https://stackoverflow.com/questions/29328785/why-does-tostring-fail-to-produce-the-correct-value-on-an-immutable-bigdecimal

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

It's not so hard to track down the reason for the odd behavior.

The divide call goes to

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

This, internally, delegates to another divide method, based on the rounding mode:

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

In this case, the last call applies. Note that the intVal (which is a BigInteger that is stored in the BigDecimal) is passed directly to this method as the first argument:

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

Finally, the path to the second divideAndRound is taken here, again passing the dividend on (which was the intVal of the original BigDecimal), ending up with this code:

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

And this is where the error is introduced: The mdivididend is a mutable BigInteger, that was created as a mutable view on the mag array of the BigInteger that is stored in the BigDecimal x from the original call. The division modifies the mag field, and thus, the state of the (now not-so-immutable) BigDecimal.

This is clearly a bug in the implementation of one of the divide methods. I already started tracking the change sets of the OpenJDK, but have not yet spotted the definite culprit. (Edit: See updates below)

(A side note: Calling x.toString() before doing the division does not really avoid, but only hide the bug: It causes a string cache of the correct state to be created internally. The right value is printed, but the internal state is still wrong - which is concerning, to say the least...)


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...)


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...