Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 271
MikeM Avatar asked Mar 29 '15 11:03

MikeM


People also ask

Is Java BigDecimal immutable?

Since BigDecimal is immutable, these operations do not modify the existing objects. Rather, they return new objects.

How accurate is BigDecimal?

This limits it to 15 to 17 decimal digits of accuracy. BigDecimal can grow to any size you need it to. Double operates in binary which means it can only precisely represent numbers which can be expressed as a finite number in binary. For example, 0.375 in binary is exactly 0.011.

What can I use instead of BigDecimal?

If you need to use division in your arithmetic, you need to use double instead of BigDecimal.


1 Answers

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

like image 170
Marco13 Avatar answered Sep 20 '22 22:09

Marco13