BullionVault course · reference

Reference card

BigDecimal & Money

Every output below was run on Temurin 25.0.3, not remembered. Print this.

The four rules

  1. Never construct from a double. Use the String constructor, or BigDecimal.valueOf.
  2. Never divide without a rounding rule. Give it a scale + RoundingMode, or a MathContext.
  3. Never compare with equals. Use compareTo(…) == 0, or pin the scale in a wrapper type.
  4. Round once, at the end, where the money lands — not at every intermediate step.

Rule 1 · the double constructor

new 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

Rule 2 · division

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
FormResulting scaleUse when
divide(d, scale, mode)the scale you asked foralmost always — you know the precision you want
divide(d, MathContext)set by significant digitsratios and rates, where magnitude varies
divide(d, mode)the dividend's scalerarely — see the trap below
divide(d)exact, or it throwsonly when you can prove it terminates
The two-argument trap

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.

In this domain

Dividing by 31.1034768 — grams per troy ounce — never terminates. The one-argument divide is always a bug on that path.

Rule 3 · equality

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

Rule 4 · rounding modes

Actual output, scale 2:

Mode2.3452.355−2.345Character
HALF_UP 2.352.36−2.35what a human means by "round"
HALF_EVEN 2.342.36−2.34banker's — unbiased over many values
CEILING 2.352.36−2.34towards +∞
FLOOR 2.342.35−2.35towards −∞
UP 2.352.36−2.35away from zero
DOWN 2.342.35−2.34towards zero — truncation
The distinction people miss

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.

Choosing a mode, in this domain

QuantityModeBecause
Execution price per kgHALF_UPa neutral conversion; no party is favoured
ConsiderationHALF_UPthe fairest reading of "to the penny"
CommissionCEILINGa stated business rule: never bill less than the rate earns
Interest, over many accrualsHALF_EVENavoids 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.

Two more traps

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.

The money-type checklist

Source

Oracle, Java 25 API — java.math.BigDecimal. Read the class-level javadoc.