BigDecimal for Money

Handle prices, tax and totals exactly with BigDecimal, choose rounding deliberately, and avoid the equals, division and constructor traps.

SyntaxBigDecimal price = new BigDecimal("19.99"); BigDecimal total = price.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_EVEN); boolean same = a.compareTo(b) == 0;

double stores binary fractions, and most decimal amounts have no exact binary form. 0.1 + 0.2 is 0.30000000000000004. Those tiny errors add up across thousands of order lines, and they surface as invoices that are a penny out or reconciliation jobs that never balance. Money needs exact decimal arithmetic: java.math.BigDecimal, or whole numbers of the smallest unit in a long.

How BigDecimal works

A BigDecimal is an integer plus a scale, the number of digits after the decimal point. 19.99 is 1999 with scale 2. It is immutable: total.add(tax) returns a new value, so writing total.add(tax); on its own line does nothing. Assign the result.

Creating values

  • new BigDecimal("19.99") from a string is exact. Use it for literals and for values parsed from requests.
  • new BigDecimal(0.1) captures the binary error exactly and gives a 55-digit number. Avoid the double constructor.
  • BigDecimal.valueOf(0.1) goes through Double.toString and gives 0.1. It is acceptable when a double is all you have, but better not to have one.

Rounding is a business decision

Multiplying keeps every digit: 59.97 times 0.20 is 11.9940. Before storing or showing a value, round it with setScale(2, RoundingMode.X). HALF_UP is what most people learnt at school; HALF_EVEN (banker's rounding) avoids a systematic upward bias across many values. Tax rules sometimes require a specific mode, or rounding per line rather than per invoice, so ask instead of guessing.

Division must be told how to round. divide with no scale throws ArithmeticException when the result does not terminate, as with 100 divided by 3. Pass a scale and a rounding mode.

equals is not what you expect

equals compares the value and the scale, so 2.0 and 2.00 are not equal and both survive in a HashSet. Use compareTo(...) == 0 for amounts, and normalise the scale before using them as map keys.

Storage and APIs

In SQL use NUMERIC or DECIMAL with an explicit scale; JDBC maps it to BigDecimal. Never store money in a FLOAT or DOUBLE column. In JSON, many APIs send amounts as strings ("19.99") or as integer minor units (1999) together with a currency code, because JavaScript clients parse numbers as doubles. Different currencies have different numbers of decimal places, such as two for GBP and none for JPY; Currency.getDefaultFractionDigits() tells you which.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Calculate order totals with VAT so the amount charged matches the invoice to the penny.
  • Split a refund or a subscription fee into instalments that always add up to the original amount.
  • Read NUMERIC columns from the database into BigDecimal and send them to clients as strings that keep every digit.

More examples

equals, compareTo and scale

HashSet uses equals and TreeSet uses compareTo, so the same two amounts give different set sizes. stripTrailingZeros can switch to exponent notation, so use toPlainString whenever the text goes to a client or a file.

Example · java
void main() {
    BigDecimal a = new BigDecimal("2.0");
    BigDecimal b = new BigDecimal("2.00");
    IO.println("equals: " + a.equals(b));
    IO.println("compareTo: " + (a.compareTo(b) == 0));

    Set<BigDecimal> hash = new HashSet<>(List.of(a, b));
    Set<BigDecimal> tree = new TreeSet<>(List.of(a, b));
    IO.println("HashSet size " + hash.size() + ", TreeSet size " + tree.size());

    BigDecimal hundred = new BigDecimal("100");
    IO.println("stripTrailingZeros: " + b.stripTrailingZeros() + ", "
            + hundred.stripTrailingZeros() + " -> " + hundred.stripTrailingZeros().toPlainString());
}

// Output:
// equals: false
// compareTo: true
// HashSet size 2, TreeSet size 1
// stripTrailingZeros: 2, 1E+2 -> 100

Division and rounding modes

HALF_EVEN rounds an exact half towards the even neighbour, so 2.345 goes down and 2.355 goes up, while HALF_UP always rounds halves away from zero. DOWN simply truncates towards zero.

Example · java
void main() {
    BigDecimal total = new BigDecimal("100.00");
    try {
        total.divide(BigDecimal.valueOf(3));
    } catch (ArithmeticException e) {
        IO.println("error: " + e.getMessage());
    }
    IO.println("100.00 / 3 = " + total.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_EVEN));

    for (String s : List.of("2.345", "2.355", "-2.345")) {
        BigDecimal v = new BigDecimal(s);
        IO.println(s + ": HALF_UP " + v.setScale(2, RoundingMode.HALF_UP)
                + ", HALF_EVEN " + v.setScale(2, RoundingMode.HALF_EVEN)
                + ", DOWN " + v.setScale(2, RoundingMode.DOWN));
    }
}

// Output:
// error: Non-terminating decimal expansion; no exact representable decimal result.
// 100.00 / 3 = 33.33
// 2.345: HALF_UP 2.35, HALF_EVEN 2.34, DOWN 2.34
// 2.355: HALF_UP 2.36, HALF_EVEN 2.36, DOWN 2.35
// -2.345: HALF_UP -2.35, HALF_EVEN -2.34, DOWN -2.34

Splitting a bill without losing a penny

Rounding each share separately would give three times 33.33 and lose a penny. Handing the leftover pennies out one at a time keeps the parts adding up to the original, which is what refunds, instalments and split payments need.

Example · java
// Assumes a positive amount in a currency with two decimal places.
static List<BigDecimal> split(BigDecimal amount, int parts) {
    BigDecimal share = amount.divide(BigDecimal.valueOf(parts), 2, RoundingMode.DOWN);
    BigDecimal remainder = amount.subtract(share.multiply(BigDecimal.valueOf(parts)));
    int extraPennies = remainder.movePointRight(2).intValueExact();

    List<BigDecimal> shares = new ArrayList<>();
    for (int i = 0; i < parts; i++) {
        shares.add(i < extraPennies ? share.add(new BigDecimal("0.01")) : share);
    }
    return shares;
}

void main() {
    for (String amount : List.of("100.00", "10.00", "0.05")) {
        List<BigDecimal> shares = split(new BigDecimal(amount), 3);
        BigDecimal sum = shares.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
        IO.println(amount + " -> " + shares + " sum " + sum);
    }
}

// Output:
// 100.00 -> [33.34, 33.33, 33.33] sum 100.00
// 10.00 -> [3.34, 3.33, 3.33] sum 10.00
// 0.05 -> [0.02, 0.02, 0.01] sum 0.05

Discussion

  • Be the first to comment on this lesson.