Exact Arithmetic with BigDecimal and BigInteger
Objective
double and long are fast because they are fixed-width binary types — and
that is exactly why they are the wrong tool for money, tax, or cryptographic
keys. A double cannot represent 0.1 at all, and a long stops at
9,223,372,036,854,775,807. java.math.BigDecimal and java.math.BigInteger
trade speed for two guarantees the primitives can't give: arbitrary precision
(the number grows as large as memory allows) and exact decimal arithmetic
under a rounding policy you choose explicitly rather than one the hardware
picks for you. Both are immutable value-like classes, so every operation
returns a new object.
Use Cases
- Monetary arithmetic — prices, invoice totals, tax, interest — where a fraction of a cent lost to binary rounding is a reconciliation bug or an audit finding.
- Anything with a legally mandated rounding rule (banker's rounding, VAT rounding, currency-specific scales) that has to be stated in code rather than inherited from IEEE 754.
- Cryptography and number theory: RSA-sized integers, modular exponentiation,
probable-prime generation — all far beyond
long. - Parsing and re-emitting decimal data (JSON, CSV, database
NUMERICcolumns) without changing the value or its number of decimal places. - Combinatorics and exact big-integer results — factorials,
2^4096, large Fibonacci numbers — where overflow would silently wrap along.
Deep Dive
Why double loses decimal values
A double stores a binary fraction. 0.1 is a repeating fraction in base 2,
so it is stored as the nearest representable value, and the error is visible
as soon as you add:
javaSystem.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
double sum = 0;
for (int i = 0; i < 10; i++) sum += 0.1;
System.out.println(sum); // 0.9999999999999999BigDecimal stores the digits you actually wrote, in base 10, so the same
computation is exact:
javaBigDecimal sum = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) sum = sum.add(new BigDecimal("0.1"));
System.out.println(sum); // 1.0Note sum = sum.add(...). BigDecimal is immutable; a bare
sum.add(x); computes a value and throws it away:
javaBigDecimal balance = new BigDecimal("10.00");
balance.add(new BigDecimal("5")); // return value discarded
System.out.println(balance); // 10.00The BigDecimal model: unscaled value and scale
A BigDecimal is exactly two things: an arbitrary-precision integer
(unscaledValue(), a BigInteger) and a 32-bit scale(). The value is
unscaledValue × 10^-scale:
javaBigDecimal d = new BigDecimal("12.3400");
System.out.println(d.unscaledValue()); // 123400
System.out.println(d.scale()); // 4
System.out.println(d.precision()); // 6 (total significant digits)Scale is part of the object's identity, which is why equals() distinguishes
2.0 from 2.00 while compareTo() does not:
javaBigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");
System.out.println(a.equals(b)); // false — different scales
System.out.println(a.compareTo(b)); // 0 — same numeric valueFor money this is a feature: 2.00 carries "two decimal places" as data, and
setScale() is how you normalise it.
javaBigDecimal price = new BigDecimal("2.345");
System.out.println(price.setScale(2, RoundingMode.HALF_UP)); // 2.35
System.out.println(price.setScale(2, RoundingMode.HALF_EVEN)); // 2.34Constructing one correctly
new BigDecimal(double) is exact — and that is the problem. It faithfully
records the binary approximation the double already holds, all 55 digits of
it:
javaSystem.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(BigDecimal.valueOf(0.1)); // 0.1
System.out.println(new BigDecimal("0.1")); // 0.1BigDecimal.valueOf(double) routes through Double.toString(), so it gives
the short decimal a human would have typed. The String constructor never
involves a double at all — prefer it whenever the value originates as text
(a request body, a CSV cell, a config file). BigInteger has the same split:
BigInteger.valueOf(long) for values that fit, the String constructor for
anything bigger.
javaBigInteger big = new BigInteger("3419229223372036854775807");
System.out.println(Long.MAX_VALUE); // 9223372036854775807
System.out.println(big); // 3419229223372036854775807Division forces you to name a rounding policy
add, subtract, and multiply always have an exact decimal answer, so the
one-argument forms are safe. Division often doesn't, and the one-argument
divide() refuses to guess:
javaBigDecimal.ONE.divide(new BigDecimal("3"));
// ArithmeticException: Non-terminating decimal expansion;
// no exact representable decimal result.Supply a target scale plus a RoundingMode, or a MathContext that fixes
the number of significant digits:
javaBigDecimal.ONE.divide(new BigDecimal("3"), 5, RoundingMode.HALF_UP);
// 0.33333
BigDecimal.ONE.divide(new BigDecimal("3"), MathContext.DECIMAL64);
// 0.3333333333333333RoundingMode (in java.math, shared with setScale) has eight constants:
UP, DOWN, CEILING, FLOOR, HALF_UP, HALF_DOWN, HALF_EVEN, and
UNNECESSARY. HALF_EVEN is banker's rounding — it rounds ties toward the
even neighbour so that a long run of roundings doesn't drift upward, which is
why it is the default in every MathContext preset except UNLIMITED.
UNNECESSARY asserts that no rounding should be needed and throws if it is:
javanew BigDecimal("2.5").setScale(0); // ArithmeticException: Rounding necessaryMathContext bundles a precision (significant digits) with a
RoundingMode, and every arithmetic method has an overload taking one:
DECIMAL32, DECIMAL64, DECIMAL128 mirror the IEEE 754 decimal formats
(7, 16, and 34 digits, all HALF_EVEN), and MathContext.UNLIMITED means
"exact, or throw".
BigInteger: unbounded integers, modular and prime arithmetic
BigInteger covers the integer operators plus the number theory that
public-key cryptography needs:
javaBigInteger.valueOf(2).pow(4096).bitLength(); // 4097
BigInteger.valueOf(48).gcd(BigInteger.valueOf(18)); // 6
BigInteger.valueOf(1000).sqrt(); // 31 (floor)
BigInteger.valueOf(4).modPow(BigInteger.valueOf(13),
BigInteger.valueOf(497)); // 445modPow is the operation behind RSA, and it is not the same as
pow().mod() — it never materialises the astronomically large intermediate.
Primality is probabilistic: isProbablePrime(certainty) and
nextProbablePrime() may report a composite as prime with probability less
than 1 - 1/2^certainty, while probablePrime(bitLength, random) generates a
fresh candidate of a given size:
javaBigInteger p = BigInteger.probablePrime(2048, new SecureRandom());
System.out.println(p.isProbablePrime(100)); // trueConstants ZERO, ONE, TWO, and TEN exist on both classes
(BigInteger.TWO since Java 9, BigDecimal.TWO since Java 19), and both
implement Comparable, so they sort and work in a TreeMap without a
comparator.
Coming back down without losing data silently
The xxxValue() methods are narrowing conversions and they are quiet about
it. A BigInteger past Double.MAX_VALUE becomes Infinity; a
BigDecimal with a fraction is truncated:
javaSystem.out.println(BigInteger.TEN.pow(400).doubleValue()); // Infinity
System.out.println(new BigDecimal("2.99").intValue()); // 2The ...Exact variants — intValueExact(), longValueExact(),
toBigIntegerExact() — fail loudly instead:
javanew BigDecimal("2.50").intValueExact();
// ArithmeticException: Rounding necessaryPrinting has a matching trap: toString() may switch to scientific notation,
toPlainString() never does.
javaBigDecimal x = new BigDecimal("600.0").stripTrailingZeros();
System.out.println(x); // 6E+2
System.out.println(x.toPlainString()); // 600Where java.math stops: no complex, no rational type
java.math is only two numeric classes. There is no built-in complex,
rational, matrix, or unsigned type — for those you either write a small value
class or take a library dependency (Apache Commons Math's Complex,
BigFraction). Since Java 16, a record makes the hand-rolled version
nearly free, and immutability comes for free with it:
javapublic record Complex(double re, double im) {
public Complex plus(Complex o) { return new Complex(re + o.re, im + o.im); }
public Complex times(Complex o) {
return new Complex(re * o.re - im * o.im, re * o.im + im * o.re);
}
public double magnitude() { return Math.hypot(re, im); }
}
var c = new Complex(3, 5).times(new Complex(2, -2));
System.out.println(c); // Complex[re=16.0, im=4.0]Note the plus/times naming: Java has no operator overloading, so every
one of these types — including BigDecimal — is used through method calls,
and a.add(b).multiply(c) is as good as the syntax gets.
Trade-offs
- Correctness costs speed and allocation — every operation allocates a
new object and runs in software instead of one CPU instruction, so
BigDecimalis orders of magnitude slower thanlongordouble. High-volume money code often stores integer minor units (cents) in alongand reaches forBigDecimalonly at the boundaries where scaling and rounding happen. equals()andcompareTo()disagree, so hash-based and sorted collections disagree too —BigDecimaldeliberately breaks the usual "consistent with equals" expectation, and2.0versus2.00is a silent duplicate key in aHashMap.javavar values = List.of(new BigDecimal("2.0"), new BigDecimal("2.00")); new HashSet<>(values).size(); // 2 — equals() sees two values new TreeSet<>(values).size(); // 1 — compareTo() sees onenew BigDecimal(double)imports the very error you switched away from — the constructor is exact about a value that was already wrong, so adoubleanywhere upstream of the conversion has already lost the precision.javanew BigDecimal(1.1).multiply(new BigDecimal("3")); // 3.300000000000000266453525910037569701671600341796875 new BigDecimal("1.1").multiply(new BigDecimal("3")); // 3.3- Unrounded division throws instead of approximating — safer than a wrong
answer, but it means every
divide()in the codebase has to make a policy decision, and a missing overload is a productionArithmeticExceptionrather than a compile error.javanew BigDecimal("10").divide(new BigDecimal("3")); // ArithmeticException - Immutability makes discarded results invisible — nothing warns when a
return value is dropped, unlike the compile error you would get from
misusing an assignment operator.java
BigDecimal total = new BigDecimal("0.00"); total.add(new BigDecimal("9.99")); // silently does nothing - Scale is unbounded, and so is cost —
BigIntegergrows with the value andBigDecimal's precision grows with everymultiply, so chained exact arithmetic on user-supplied input can consume surprising amounts of memory and CPU.MathContexton each operation, or a periodicsetScale(), caps it. - Readability suffers — a formula written in
BigDecimalmethod calls is materially harder to read and review than the same formula in operators, which is a real argument for keeping the exact-arithmetic layer thin and well-tested rather than spreading it through the domain model.