BullionVault course · reference
Reference card
Every output below was run on Temurin 25.0.3, not remembered. Print this.
double. Use the String constructor, or BigDecimal.valueOf.divide without a rounding rule. Give it a scale + RoundingMode, or a MathContext.equals. Use compareTo(…) == 0, or pin the scale in a wrapper type.double constructornew BigDecimal(0.1) → 0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(0.1) → 0.1
new BigDecimal("0.1") → 0.1
new BigDecimal(double) is faithful to the binary value the
double actually holds — which is not the number you typed.
valueOf goes via Double.toString and gives you the
number you meant. If the value came from a human, a config file, or a feed, it
was a String: keep it one.
0.1 + 0.2 as double → 0.30000000000000004
0.1 + 0.2 as BigDecimal → 0.3
new BigDecimal("1").divide(new BigDecimal("3"))
→ ArithmeticException: Non-terminating decimal expansion;
no exact representable decimal result.
new BigDecimal("1").divide(new BigDecimal("3"), 6, RoundingMode.HALF_UP)
→ 0.333333
new BigDecimal("1").divide(new BigDecimal("3"), MathContext.DECIMAL64)
→ 0.3333333333333333
| Form | Resulting scale | Use when |
|---|---|---|
| divide(d, scale, mode) | the scale you asked for | almost always — you know the precision you want |
| divide(d, MathContext) | set by significant digits | ratios and rates, where magnitude varies |
| divide(d, mode) | the dividend's scale | rarely — see the trap below |
| divide(d) | exact, or it throws | only when you can prove it terminates |
divide(divisor, RoundingMode) looks like it fixes the
non-terminating problem, and it does — but it silently inherits the
dividend's scale, which is almost never the scale you wanted:
BigDecimal dividend = new BigDecimal("3000.00").multiply(BigDecimal.valueOf(1000));
// 3000000.00 — scale 2, inherited from "3000.00"
dividend.divide(OZ, RoundingMode.HALF_UP) → 96452.24 (scale 2)
dividend.divide(OZ, 6, RoundingMode.HALF_UP) → 96452.239706 (scale 6)
Your rounding mode was right and the answer is still wrong, because the scale came from wherever the dividend happened to get its own. Two independent decisions — state both.
Dividing by 31.1034768 — grams per troy ounce — never
terminates. The one-argument divide is always a bug on that path.
new BigDecimal("1.50").equals(new BigDecimal("1.5")) → false
new BigDecimal("1.50").compareTo(new BigDecimal("1.5")) → 0
hashCodes differ too — so BigDecimal is unsafe as a HashMap key
or in a HashSet, and assertEquals on two of them can fail
while printing what looks like the same number.
BigDecimal's javadoc says it outright: its natural ordering is
inconsistent with equals. Two fixes:
// at the call site
if (a.compareTo(b) == 0) { … }
// or once, in a type — what Money does
public record Money(BigDecimal amount, Currency currency) {
public Money {
amount = amount.setScale(currency.getDefaultFractionDigits(),
RoundingMode.UNNECESSARY);
}
}
Pinning the scale makes the record's generated equals correct for
free. UNNECESSARY additionally means the type refuses to round
behind your back:
new BigDecimal("1.005").setScale(2, RoundingMode.UNNECESSARY)
→ ArithmeticException: Rounding necessary
Actual output, scale 2:
| Mode | 2.345 | 2.355 | −2.345 | Character |
|---|---|---|---|---|
| HALF_UP | 2.35 | 2.36 | −2.35 | what a human means by "round" |
| HALF_EVEN | 2.34 | 2.36 | −2.34 | banker's — unbiased over many values |
| CEILING | 2.35 | 2.36 | −2.34 | towards +∞ |
| FLOOR | 2.34 | 2.35 | −2.35 | towards −∞ |
| UP | 2.35 | 2.36 | −2.35 | away from zero |
| DOWN | 2.34 | 2.35 | −2.34 | towards zero — truncation |
CEILING/FLOOR care about the number
line; UP/DOWN care about zero.
They agree on positives and disagree on negatives. On a trading system where a
quantity can be a sell, that difference is a directional bias in your P&L.
And HALF_EVEN on 2.345 gives 2.34, not 2.35 —
it rounds the half to the nearest even last digit. That is the point:
over many roundings it does not drift upward the way HALF_UP does.
| Quantity | Mode | Because |
|---|---|---|
| Execution price per kg | HALF_UP | a neutral conversion; no party is favoured |
| Consideration | HALF_UP | the fairest reading of "to the penny" |
| Commission | CEILING | a stated business rule: never bill less than the rate earns |
| Interest, over many accruals | HALF_EVEN | avoids systematic drift in one direction |
The mode is a business decision. If the spec does not state one, that is a question to ask, not a default to pick silently.
new BigDecimal("600").stripTrailingZeros() → 6E+2
new BigDecimal("600").stripTrailingZeros().toPlainString() → 600
toString() switches to scientific notation for some scales.
Anything user-facing or written to a file wants toPlainString().
Second: BigDecimal is immutable. Every operation
returns a new instance. value.add(other); on its own line does
nothing at all, and the compiler will not warn you.
BigDecimal crossing a method boundary.equals is safe.double anywhere in the type, including in factory methods.toString shows the currency code and uses toPlainString.Oracle, Java 25 API — java.math.BigDecimal. Read the class-level javadoc.