BigDecimal for Money
Handle prices, tax and totals exactly with BigDecimal, choose rounding deliberately, and avoid the equals, division and constructor traps.
BigDecimal 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 thedoubleconstructor.BigDecimal.valueOf(0.1)goes throughDouble.toStringand gives0.1. It is acceptable when adoubleis 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
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.
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 -> 100Division 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.
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.34Splitting 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.
// 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