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
@MikeMsaid: Bug has been listed on openjdk bug list and it has been resolved inJDK8 Build 51Update : 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…)