Contents
- Why BigDecimal & BigInteger
- Creating BigDecimal
- BigDecimal Arithmetic
- Scale, Precision & Rounding
- BigDecimal Division Pitfalls
- Comparing BigDecimal
- BigInteger Essentials
- Formatting & Parsing
- Performance & Alternatives
- Real-World Patterns & Best Practices
Before you write a single line of financial code in Java, run this program. It is the best 30-second
education in numerical computing you will ever get:
public class FloatingPointTrap {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 0.30000000000000004
System.out.println(a + b == 0.3); // false
double price = 1.10;
double qty = 3;
System.out.println(price * qty); // 3.3000000000000003
}
}
That extra 0.00000000000000004 is not a bug in Java — it's a direct consequence of how
IEEE 754 binary floating-point works. A double stores numbers as
sign × mantissa × 2exponent. The fraction 0.1 in base 2 is an
infinitely repeating pattern (0.0001100110011…), just like 1/3 is in base 10.
The hardware truncates it, and every subsequent operation compounds the tiny error.
A double has about 15–17 decimal digits of precision. For scientific work where
an error in the 16th digit is irrelevant, it's perfect. For a bank statement where a single
cent must be correct across billions of transactions, it is catastrophic.
The long overflow trap
Integers have a different failure mode: silent overflow. A long holds values up to
about 9.2 × 1018. Factorials, RSA keys, and combinatorial counts blow past that instantly:
long factorial = 1;
for (int i = 1; i <= 25; i++) {
factorial *= i;
}
System.out.println(factorial); // 7034535277573963776 (WRONG — overflow at 21!)
// BigInteger handles it effortlessly:
java.math.BigInteger big = java.math.BigInteger.ONE;
for (int i = 1; i <= 25; i++) {
big = big.multiply(java.math.BigInteger.valueOf(i));
}
System.out.println(big); // 15511210043330985984000000
When you must reach for BigDecimal and BigInteger
- Money — currency amounts, invoicing, tax, interest, accounting ledgers.
Regulators, auditors, and users all demand exact cents.
- High-precision scientific — calculations where accumulated rounding error
would distort a result over millions of iterations.
- Cryptography — RSA, Diffie-Hellman, and elliptic-curve math operate on
integers with hundreds or thousands of digits. That's BigInteger's home turf.
- Combinatorics — factorials, binomial coefficients, and counting problems
that overflow long.
- Any arithmetic on untrusted or user-supplied decimals where the result
must exactly match the input's precision.
The fix for both traps lives in the same package — java.math:
import java.math.BigDecimal;
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3 (exact)
System.out.println(a.add(b).equals(new BigDecimal("0.3"))); // true
The single most important rule when creating a BigDecimal is: always use
the String constructor, never the double constructor.
The new BigDecimal(double) trap. The double constructor
converts whatever inexact binary approximation the JVM stored — and faithfully preserves
that error into your BigDecimal. Watch:
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1"));
// 0.1 (exactly what you wanted)
The String constructor parses the decimal digits directly and never touches
floating-point hardware, so what you type is what you get.
The creation cheat-sheet
import java.math.BigDecimal;
// 1. String constructor — always safe, always preferred
BigDecimal a = new BigDecimal("1.23");
BigDecimal b = new BigDecimal("9999999999999999999.9999"); // arbitrary size, no overflow
// 2. valueOf(double) — also safe; internally uses Double.toString()
BigDecimal c = BigDecimal.valueOf(0.1); // 0.1 (good)
BigDecimal d = BigDecimal.valueOf(1.23); // 1.23
// 3. valueOf(long) — safe for integer values
BigDecimal e = BigDecimal.valueOf(1000L); // 1000
// 4. valueOf(long unscaledValue, int scale) — store raw digits + decimal position
BigDecimal f = BigDecimal.valueOf(12345L, 2); // 123.45
// 5. Constants — use these, don't create new objects
BigDecimal zero = BigDecimal.ZERO; // 0
BigDecimal one = BigDecimal.ONE; // 1
BigDecimal ten = BigDecimal.TEN; // 10
// 6. From int or long
BigDecimal g = new BigDecimal(42); // 42 (int constructor is safe)
BigDecimal h = new BigDecimal(1_000_000_000L); // 1000000000
// 7. NEVER do this in production code
BigDecimal bad = new BigDecimal(0.1);
System.out.println(bad);
// 0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(double) is a convenient shortcut that calls
Double.toString(d) under the hood. It's safe for literal doubles, but if the
double was itself the result of arithmetic it may already carry IEEE 754 error.
When in doubt, construct from String.
Int / long constructors are safe
Unlike double, int and long represent integers exactly, so
new BigDecimal(42) is fine. The danger is specifically with
float and double.
BigDecimal is immutable. Every arithmetic method returns a
new instance; it never mutates the receiver. This means:
BigDecimal a = new BigDecimal("10.00");
a.add(new BigDecimal("5.00")); // result discarded!
System.out.println(a); // 10.00 — unchanged
// Must assign the result:
a = a.add(new BigDecimal("5.00"));
System.out.println(a); // 15.00
A common bug: calling total.add(item) and forgetting to reassign. The compiler won't
warn you — the result is simply thrown away. Treat BigDecimal like String: always use
the return value.
Core operations
import java.math.BigDecimal;
BigDecimal x = new BigDecimal("12.50");
BigDecimal y = new BigDecimal("3.20");
BigDecimal sum = x.add(y); // 15.70
BigDecimal diff = x.subtract(y); // 9.30
BigDecimal product = x.multiply(y); // 40.0000 (scales add up!)
BigDecimal quot = x.divide(y, 4, java.math.RoundingMode.HALF_UP); // 3.9063
BigDecimal rem = x.remainder(y); // 2.90
BigDecimal squared = x.pow(2); // 156.2500
BigDecimal neg = x.negate(); // -12.50
BigDecimal abs = neg.abs(); // 12.50
Note multiply's scale: 12.50 (scale 2) × 3.20 (scale 2) = 40.0000 (scale 4).
Scales add on multiply and subtract on divide. If you want a fixed scale
back, call setScale afterwards (covered next).
Method chaining — a worked invoice example
Because every method returns a new BigDecimal, you can chain fluently. Here's a realistic
invoice total with line items, a 10% discount, and 8.25% sales tax:
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Invoice {
public static void main(String[] args) {
BigDecimal item1 = new BigDecimal("49.99").multiply(new BigDecimal("2")); // 2 × $49.99
BigDecimal item2 = new BigDecimal("19.95");
BigDecimal item3 = new BigDecimal("7.49").multiply(new BigDecimal("4")); // 4 × $7.49
BigDecimal subtotal = item1.add(item2).add(item3);
// 99.98 + 19.95 + 29.96 = 149.89
BigDecimal discount = subtotal.multiply(new BigDecimal("0.10"))
.setScale(2, RoundingMode.HALF_UP);
// 14.99
BigDecimal afterDiscount = subtotal.subtract(discount); // 134.90
BigDecimal tax = afterDiscount.multiply(new BigDecimal("0.0825"))
.setScale(2, RoundingMode.HALF_UP);
// 11.13
BigDecimal total = afterDiscount.add(tax); // 146.03
System.out.println("Subtotal: $" + subtotal);
System.out.println("Discount: -$" + discount);
System.out.println("Tax: $" + tax);
System.out.println("Total: $" + total);
}
}
Each step returns a new object; the chain reads naturally. Notice we call
setScale(2, HALF_UP) after multiplication to snap back to two decimal places —
which brings us to the heart of BigDecimal: scale and rounding.
Two concepts confuse newcomers: scale and precision.
They are not the same thing.
- Scale = the number of digits after the decimal point.
123.4500 has scale 4.
- Precision = the total number of significant digits.
123.4500 has precision 7.
BigDecimal n = new BigDecimal("123.4500");
System.out.println(n.scale()); // 4
System.out.println(n.precision()); // 7
System.out.println(n.unscaledValue()); // 1234500 (the digits as a BigInteger)
Internally, a BigDecimal stores an unscaled BigInteger and an int scale.
The value is unscaledValue × 10-scale. That's why it represents decimal
fractions exactly: it's literally a base-10 integer plus a power-of-ten divisor.
setScale — the workhorse
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal n = new BigDecimal("2.7182818284");
n.setScale(2, RoundingMode.HALF_UP); // 2.72
n.setScale(4, RoundingMode.HALF_UP); // 2.7183
n.setScale(6, RoundingMode.DOWN); // 2.718281
n.setScale(10); // 2.7182818284 (no rounding needed)
n.setScale(2); // ArithmeticException — rounding needed, no mode supplied
The 8 RoundingMode options
Every RoundingMode controls what happens when discarded digits would otherwise change the
result. Here's the complete table, showing each mode applied to 2.5 and 2.55
rounded to one decimal place less:
| Mode |
Meaning |
2.5 → 0 dp |
2.55 → 1 dp |
-2.5 → 0 dp |
| UP | Away from zero | 3 | 2.6 | -3 |
| DOWN | Toward zero (truncate) | 2 | 2.5 | -2 |
| CEILING | Toward +∞ | 3 | 2.6 | -2 |
| FLOOR | Toward −∞ | 2 | 2.5 | -3 |
| HALF_UP | Nearest; ties go away from zero | 3 | 2.6 | -3 |
| HALF_DOWN | Nearest; ties go toward zero | 2 | 2.5 | -2 |
| HALF_EVEN | Banker's rounding; ties go to even neighbor | 2 | 2.6 | -2 |
| UNNECESSARY | Assert no rounding needed; throws otherwise | — | — | — |
Why HALF_EVEN is the IEEE 754 default. If you always round halves up, you
introduce a statistical bias toward larger numbers in any long sum. HALF_EVEN (banker's
rounding) alternates — 2.5 rounds down to 2, 3.5 rounds up to 4 — so errors cancel out over
many operations. This is why most financial regulators and standards specify it.
Seeing the modes in action
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal half = new BigDecimal("2.5");
for (RoundingMode mode : RoundingMode.values()) {
if (mode == RoundingMode.UNNECESSARY) continue;
System.out.printf("%-12s %s%n", mode, half.setScale(0, mode));
}
// UP 3
// DOWN 2
// CEILING 3
// FLOOR 2
// HALF_UP 3
// HALF_DOWN 2
// HALF_EVEN 2
Don't use deprecated BigDecimal.ROUND_* int constants. Pre-Java 9
code used integer constants like BigDecimal.ROUND_HALF_UP. These are deprecated —
always use the RoundingMode enum instead.
Division is where most BigDecimal bugs live. Because BigDecimal stores exact
base-10 fractions, it cannot represent results like 1/3 = 0.3333… — the digits
repeat forever. So when you ask it to do impossible division without guidance, it throws.
ArithmeticException: Non-terminating decimal expansion.
The one-argument divide(BigDecimal) method only works when the quotient has a
finite decimal representation. Otherwise it throws:
BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
BigDecimal c = a.divide(b);
// Exception in thread "main" java.lang.ArithmeticException:
// Non-terminating decimal expansion; no exact representable decimal result.
The fix is to always specify a scale and RoundingMode:
BigDecimal c = a.divide(b, 10, RoundingMode.HALF_UP);
// 0.3333333333
All the divide overloads
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
BigDecimal a = new BigDecimal("10.00");
BigDecimal b = new BigDecimal("3");
// 1. scale + mode — most common
a.divide(b, 4, RoundingMode.HALF_UP); // 3.3333
// 2. MathContext — precision-based rounding
a.divide(b, new MathContext(6)); // 3.33333 (6 significant digits)
a.divide(b, MathContext.DECIMAL64); // IEEE 754 double-equivalent precision
a.divide(b, MathContext.DECIMAL128); // quad-precision
// 3. divideAndRemainder — returns [quotient, remainder]
BigDecimal[] qr = a.divideAndRemainder(b);
// qr[0] = 3, qr[1] = 1.00
// 4. divideToIntegralValue — integer part of quotient
a.divideToIntegralValue(b); // 3
Practical: splitting revenue between partners
Imagine you need to split $100.00 between three partners. You can't give each
$33.3333…, and rounding each share to $33.33 loses a penny. The
standard accounting fix is to give the remainder to one partner:
import java.math.BigDecimal;
import java.math.RoundingMode;
public class RevenueSplit {
public static void main(String[] args) {
BigDecimal total = new BigDecimal("100.00");
BigDecimal partners = new BigDecimal("3");
// Each partner's fair share, rounded down to cents
BigDecimal share = total.divide(partners, 2, RoundingMode.DOWN);
// 33.33
BigDecimal distributed = share.multiply(partners); // 99.99
BigDecimal remainder = total.subtract(distributed); // 0.01
System.out.println("Partner A: $" + share.add(remainder)); // 33.34
System.out.println("Partner B: $" + share); // 33.33
System.out.println("Partner C: $" + share); // 33.33
// Total distributed = 100.00 exactly
}
}
The principle: never let money disappear. Always compute the rounded shares,
sum them, and route the rounding residue somewhere explicit. Regulators call this
"largest-remainder" or "residual" allocation.
Comparing BigDecimals looks innocent but hides a famous gotcha that has broken more
financial test suites than any other.
equals() also checks scale. Two BigDecimals are only
equals if they have the same value and the same scale. 2.0
and 2.00 are mathematically identical but have different scales, so:
BigDecimal x = new BigDecimal("2.0");
BigDecimal y = new BigDecimal("2.00");
System.out.println(x.equals(y)); // false (!!!)
System.out.println(x.compareTo(y)); // 0 (equal numerically)
System.out.println(x.compareTo(y) == 0); // true
Rule: for numeric equality, always use
compareTo(...) == 0. Reserve equals for cases where you specifically
care that two values have identical representations (rare).
Ordering and sorting
import java.math.BigDecimal;
import java.util.*;
List<BigDecimal> prices = new ArrayList<>(List.of(
new BigDecimal("19.99"),
new BigDecimal("4.50"),
new BigDecimal("100.00"),
new BigDecimal("4.5") // same value as 4.50, different scale
));
prices.sort(Comparator.naturalOrder());
// [4.50, 4.5, 19.99, 100.00] — 4.50 and 4.5 are adjacent (compareTo treats them equal)
BigDecimal smallest = Collections.min(prices); // 4.50
BigDecimal biggest = Collections.max(prices); // 100.00
Built-in min / max
BigDecimal a = new BigDecimal("10.50");
BigDecimal b = new BigDecimal("10.49");
BigDecimal lower = a.min(b); // 10.49
BigDecimal higher = a.max(b); // 10.50
// Common pattern: clamp to a minimum
BigDecimal price = userInput.max(BigDecimal.ZERO); // never go negative
Never put BigDecimals in a HashSet or as HashMap keys
expecting value-based lookup. HashSet uses equals, so
2.0 and 2.00 are stored as separate entries. If you need value-based
uniqueness, either normalize to a canonical scale via setScale first, or use a
TreeSet with Comparator.naturalOrder() (which uses compareTo).
BigInteger is BigDecimal's simpler cousin — arbitrary-precision whole numbers,
no decimal point, no scale. It shines wherever long's 64-bit ceiling isn't
enough: factorials, combinatorics, and especially cryptography.
Creating and basic arithmetic
import java.math.BigInteger;
BigInteger a = new BigInteger("123456789012345678901234567890");
BigInteger b = BigInteger.valueOf(1_000_000_000L);
BigInteger z = BigInteger.ZERO;
BigInteger o = BigInteger.ONE;
BigInteger t = BigInteger.TEN;
BigInteger two = BigInteger.TWO; // Java 9+
a.add(b); // 123456789012345678902234567890
a.subtract(b); // 123456789012345678900234567890
a.multiply(b); // 123456789012345678901234567890000000000
a.divide(b); // 123456789012345678901 (integer division)
a.mod(b); // 234567890
a.pow(5); // enormous
a.gcd(b); // greatest common divisor
a.negate().abs(); // absolute value
Modular arithmetic for cryptography
RSA, Diffie-Hellman, and most public-key crypto rely on modular exponentiation — raising a
number to a large power and taking the result mod some prime. Naively that would blow up to
astronomical sizes, but modPow computes it efficiently in place:
import java.math.BigInteger;
import java.security.SecureRandom;
public class MiniRSA {
public static void main(String[] args) {
SecureRandom rng = new SecureRandom();
// Generate two 1024-bit primes
BigInteger p = BigInteger.probablePrime(1024, rng);
BigInteger q = BigInteger.probablePrime(1024, rng);
BigInteger n = p.multiply(q); // modulus
BigInteger phi = p.subtract(BigInteger.ONE)
.multiply(q.subtract(BigInteger.ONE));
BigInteger e = BigInteger.valueOf(65537); // public exponent
BigInteger d = e.modInverse(phi); // private exponent
BigInteger msg = new BigInteger("42");
BigInteger encrypted = msg.modPow(e, n); // c = m^e mod n
BigInteger decrypted = encrypted.modPow(d, n); // m = c^d mod n
System.out.println(decrypted); // 42
}
}
modPow, modInverse, and probablePrime are the three
primitives behind most public-key cryptography. They're implemented in heavily optimized
native code — you can build a toy RSA in a dozen lines, which is both empowering and a
reminder that you should never actually use your own crypto in production.
Bit operations
import java.math.BigInteger;
BigInteger n = BigInteger.valueOf(0b1010_1100); // 172
n.bitLength(); // 8
n.bitCount(); // 4 (popcount)
n.testBit(2); // true (bit 2 is set in 172)
n.setBit(0); // 173
n.clearBit(5); // 140
n.flipBit(7); // 44
n.shiftLeft(3); // 1376 (172 × 8)
n.shiftRight(2); // 43 (172 / 4)
n.and(BigInteger.valueOf(0xFF)); // 172
n.or(BigInteger.valueOf(0x100)); // 428
n.xor(BigInteger.valueOf(0xFF)); // 83
n.not(); // -173
Prime testing
import java.math.BigInteger;
import java.security.SecureRandom;
BigInteger n = new BigInteger("104729");
n.isProbablePrime(20); // true — certainty 1 - (1/2)^20
// Generate a random probable prime of a given bit length
BigInteger p = BigInteger.probablePrime(256, new SecureRandom());
// Find the next probable prime >= n
BigInteger next = n.nextProbablePrime();
// 104743
isProbablePrime(certainty) uses Miller-Rabin. A certainty of 20 gives you a
false-positive probability of less than one in a million; 100 makes it astronomically small.
For cryptographic use, a certainty of 80–100 is standard.
Displaying BigDecimals to users is not as simple as calling toString(). There
are three string methods, each with different behavior:
import java.math.BigDecimal;
BigDecimal small = new BigDecimal("0.0000123");
BigDecimal huge = new BigDecimal("123000000");
small.toString(); // "1.23E-5" (scientific!)
small.toPlainString(); // "0.0000123" (no exponent)
small.toEngineeringString(); // "12.3E-6" (engineering notation)
huge.toString(); // "123000000"
huge.toPlainString(); // "123000000"
huge.toEngineeringString(); // "123E+6"
toString() uses scientific notation for small or very large numbers. If you're
writing BigDecimals to JSON, CSVs, or databases, always use
toPlainString() — otherwise "1.23E-5" will confuse downstream
parsers.
The stripTrailingZeros display gotcha
BigDecimal n = new BigDecimal("100.000");
BigDecimal stripped = n.stripTrailingZeros();
System.out.println(stripped.toString()); // 1E+2 (WAT!)
System.out.println(stripped.toPlainString()); // 100 (fine)
stripTrailingZeros returns 100 with scale -2, which prints
as "1E+2" in scientific notation. Always pair it with toPlainString(),
or use setScale(0) if you want a specific result.
Locale-aware currency formatting
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.Locale;
BigDecimal amount = new BigDecimal("1234567.89");
NumberFormat us = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat de = NumberFormat.getCurrencyInstance(Locale.GERMANY);
NumberFormat jp = NumberFormat.getCurrencyInstance(Locale.JAPAN);
System.out.println(us.format(amount)); // $1,234,567.89
System.out.println(de.format(amount)); // 1.234.567,89 €
System.out.println(jp.format(amount)); // ¥1,234,568 (yen has no fractional unit)
Custom patterns with DecimalFormat
import java.math.BigDecimal;
import java.text.DecimalFormat;
BigDecimal n = new BigDecimal("1234567.8");
DecimalFormat money = new DecimalFormat("#,##0.00");
DecimalFormat percent = new DecimalFormat("0.00%");
DecimalFormat scientific = new DecimalFormat("0.###E0");
System.out.println(money.format(n)); // 1,234,567.80
System.out.println(percent.format(new BigDecimal("0.0825"))); // 8.25%
System.out.println(scientific.format(n)); // 1.235E6
DecimalFormat accepts BigDecimal directly but internally may route through
double for some patterns. For guaranteed precision in output, configure it with
setParseBigDecimal(true) and when extreme precision matters, do your own digit
formatting using toPlainString() and string manipulation.
Parsing user input safely
import java.math.BigDecimal;
public static BigDecimal parseAmount(String input) {
if (input == null) return null;
String cleaned = input.trim().replace(",", "").replace("$", "");
try {
return new BigDecimal(cleaned);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid amount: " + input, e);
}
}
parseAmount("$1,234.56"); // 1234.56
parseAmount(" 42.00 "); // 42.00
parseAmount("abc"); // IllegalArgumentException
Correctness has a price. BigDecimal arithmetic is roughly 50–100×
slower than double and uses much more memory — each operation allocates
a new object, stresses the garbage collector, and takes many instructions instead of one.
For most applications that cost is negligible, but in hot loops or high-frequency trading
it can dominate.
When each representation makes sense
| Representation |
Precision |
Speed |
Memory |
Best for |
Avoid for |
| double |
~15–17 sig digits, binary |
Fastest (1×) |
8 bytes |
Scientific calc, graphics, ML, physics |
Money, currencies, exact decimals |
| long as cents |
Exact, fixed 2 dp |
Very fast (~1×) |
8 bytes |
High-volume money within one currency |
Tax math, multi-currency, division |
| BigDecimal |
Arbitrary, exact |
Slow (~50–100×) |
~40+ bytes |
General money, accounting, tax, invoices |
Inner loops of number-crunching code |
| BigInteger |
Arbitrary integer |
Slow (grows with size) |
Varies |
Crypto, combinatorics, huge counters |
Decimals, normal counting |
The "long cents" pattern
For very high-throughput systems (ad tech, exchanges) you can sidestep BigDecimal entirely
by storing money as an integer count of the smallest unit — pennies, satoshis, millicents,
whatever. All arithmetic is then primitive long addition, blazingly fast:
public class Money {
private final long cents;
private Money(long cents) { this.cents = cents; }
public static Money dollars(long d) { return new Money(d * 100L); }
public static Money ofCents(long c) { return new Money(c); }
public Money plus(Money other) { return new Money(this.cents + other.cents); }
public Money times(int n) { return new Money(this.cents * n); }
@Override public String toString() {
return String.format("$%d.%02d", cents / 100, Math.abs(cents % 100));
}
}
Money a = Money.dollars(10); // $10.00
Money b = Money.ofCents(250); // $2.50
System.out.println(a.plus(b)); // $12.50
System.out.println(a.times(3)); // $30.00
The "long cents" pattern breaks down for tax and interest math: multiplying
by 0.0825 needs fractional cents to stay exact. Either drop back to BigDecimal
for the computation and convert, or store amounts in sub-cents (e.g., 1/10,000 of a
currency unit — the same approach used by Oracle's NUMERIC(19, 4)).
Performance tips if you stay with BigDecimal
- Reuse constants. BigDecimal.ZERO, ONE,
TEN are cached — never write new BigDecimal(0).
- Cache your multipliers. Declaring static final BigDecimal TAX =
new BigDecimal("0.0825") avoids allocating it on every call.
- Use MathContext instead of unbounded precision for
intermediate calculations in long chains. DECIMAL64 is a good middle ground.
- Batch scale normalization. Don't call setScale after every
step — do it once at the end of a chain where the result is surfaced.
- Avoid stripTrailingZeros in hot paths — it allocates and
the scale shift is rarely worth it.
Here's how BigDecimal and BigInteger actually show up in production Java applications.
The Money class pattern
Raw BigDecimal has no concept of currency — $10 and €10
look the same. Wrap it in a domain class to enforce type safety and centralize rounding
rules:
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;
public final class Money {
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) {
this.amount = Objects.requireNonNull(amount)
.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
this.currency = Objects.requireNonNull(currency);
}
public static Money of(String amount, String currencyCode) {
return new Money(new BigDecimal(amount), Currency.getInstance(currencyCode));
}
public Money plus(Money other) {
requireSameCurrency(other);
return new Money(this.amount.add(other.amount), currency);
}
public Money minus(Money other) {
requireSameCurrency(other);
return new Money(this.amount.subtract(other.amount), currency);
}
public Money times(BigDecimal factor) {
return new Money(this.amount.multiply(factor), currency);
}
public boolean isGreaterThan(Money other) {
requireSameCurrency(other);
return this.amount.compareTo(other.amount) > 0;
}
private void requireSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException(
"Currency mismatch: " + currency + " vs " + other.currency);
}
}
@Override public boolean equals(Object o) {
if (!(o instanceof Money m)) return false;
return amount.compareTo(m.amount) == 0 && currency.equals(m.currency);
}
@Override public int hashCode() {
return Objects.hash(amount.stripTrailingZeros(), currency);
}
@Override public String toString() {
return currency.getSymbol() + amount.toPlainString();
}
}
// Usage
Money invoice = Money.of("149.89", "USD")
.times(new BigDecimal("1.0825")); // + 8.25% tax
// $162.25 (rounded HALF_EVEN to 2 dp)
Notice the equals method uses compareTo (not BigDecimal's own
equals), and hashCode normalizes via stripTrailingZeros
to stay consistent with equals. This is the only safe way to put Money in a HashMap.
JPA / Hibernate mapping
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import java.math.BigDecimal;
@Entity
public class Invoice {
// precision = total digits, scale = digits after decimal
// 19 total, 4 after decimal — industry standard for money with sub-cent accuracy
@Column(precision = 19, scale = 4, nullable = false)
private BigDecimal subtotal;
@Column(precision = 19, scale = 4, nullable = false)
private BigDecimal tax;
@Column(precision = 19, scale = 4, nullable = false)
private BigDecimal total;
}
On the database side, this maps to NUMERIC(19, 4) in PostgreSQL/Oracle or
DECIMAL(19, 4) in MySQL/SQL Server. Never use DOUBLE, FLOAT,
or REAL for money columns — they carry the same IEEE 754 error into your database.
Jackson JSON serialization
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
ObjectMapper mapper = new ObjectMapper();
// Write full plain-string form (no scientific notation)
mapper.enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN);
// Preserve BigDecimal precision on parse — don't silently convert to double
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
Without WRITE_BIGDECIMAL_AS_PLAIN, Jackson may emit
"0.0000123" as "1.23E-5", breaking downstream JSON consumers that
expect plain numbers. Always enable both flags for financial APIs.
The tax multiplication pitfall
Never multiply BigDecimal by a double literal.
// WRONG — 0.08 becomes 0.080000000000000002081668...
BigDecimal tax = amount.multiply(BigDecimal.valueOf(0.08));
// RIGHT — exact 0.08
BigDecimal tax = amount.multiply(new BigDecimal("0.08"));
// Also RIGHT — declare it as a constant
private static final BigDecimal TAX_RATE = new BigDecimal("0.08");
BigDecimal tax = amount.multiply(TAX_RATE);
BigDecimal.valueOf(0.08) actually produces 0.08 correctly (via
Double.toString), but new BigDecimal(0.08) does not. The safest
habit is to always type decimal rates as string literals.
Best practices summary
- Always use the String constructor for literal BigDecimals.
- Always use compareTo(...) == 0 for numeric equality, never
equals.
- Always specify scale and RoundingMode when dividing. One-arg
divide will throw on any repeating decimal.
- Pick HALF_EVEN for money unless your business rules specify
otherwise. It's the IEEE 754 default and the standard for most financial regulation.
- Store money in database NUMERIC(19, 4), not
DOUBLE.
- Use toPlainString() for output to JSON, CSV, and logs.
- Wrap BigDecimal in a domain Money class so currency is type-checked and
rounding is centralized.
- Cache constants like tax rates as static final BigDecimal.
- Profile before micro-optimizing. BigDecimal is slow per operation but
rarely the bottleneck; only move to long-cents if profiling says so.
- Never use double for money. Not for display, not for
"temporary" calculations, not ever. Once a double touches the pipeline, precision is lost.
Get these habits right and your financial code will be boring — which, when money is on the
line, is exactly what you want.