BigDecimal
BigDecimal provides exact, arbitrary-precision decimal arithmetic — the correct tool for money and any calculation where floating-point rounding error is unacceptable.
BigDecimal (java.math.BigDecimal) represents a signed decimal number exactly, as an arbitrary-precision unscaled integer value together with a 32-bit scale indicating the number of digits to the right of the decimal point. Unlike double/float, which approximate decimal values in binary floating point, BigDecimal represents decimal values exactly, making it the standard choice for monetary calculations and any domain where exact decimal results are a requirement, not a nice-to-have.
Why BigDecimal Exists: The Floating-Point Problem
double and float use IEEE 754 binary floating-point representation, which cannot exactly represent most decimal fractions — 0.1 has no exact finite binary representation, so it is stored as the closest approximate value. Accumulated over many operations, these tiny approximation errors compound, and for money, where cents must reconcile exactly, this is unacceptable. BigDecimal solves this by representing the value as an exact decimal (an integer times 10^-scale), with no binary approximation involved.
System.out.println(0.1 + 0.2); // 0.30000000000000004 -- double, imprecise
import java.math.BigDecimal;
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3 -- exactConstructors: Why the String Constructor Matters
BigDecimal has several constructors, but two behave very differently: new BigDecimal(String) parses the exact decimal text you provide, while new BigDecimal(double) captures the exact (often surprising) binary floating-point value already stored in that double — which is usually NOT the decimal value the literal appears to represent, because the double itself was already an approximation before BigDecimal ever saw it.
This is why the String constructor (or the equivalent BigDecimal.valueOf(double), which goes through Double.toString() first) should be used whenever the intended value is a decimal literal — never new BigDecimal(double) directly for that purpose.
BigDecimal.valueOf(double) is different from new BigDecimal(double): valueOf() converts through Double.toString(d) first, giving the decimal value the double's canonical string representation shows, which usually matches programmer intent far better than the raw constructor.
BigDecimal fromString = new BigDecimal("0.1");
System.out.println(fromString); // 0.1 -- exact
BigDecimal fromDouble = new BigDecimal(0.1);
System.out.println(fromDouble);
// 0.1000000000000000055511151231257827021181583404541015625
// -- the exact binary value of the double 0.1, not the decimal "0.1"Scale: Digits After the Decimal Point
Scale is the number of digits to the right of the decimal point; a BigDecimal's value equals unscaledValue x 10^-scale. "1.50" and "1.5" represent the same mathematical value but have different scales (2 and 1 respectively) and are NOT equal() (though they ARE compareTo()-equal — see below). setScale(int, RoundingMode) changes the scale explicitly, rounding as specified when reducing precision.
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal a = new BigDecimal("1.50");
System.out.println(a.scale()); // 2
System.out.println(a.unscaledValue()); // 150
BigDecimal rounded = new BigDecimal("19.995").setScale(2, RoundingMode.HALF_UP);
System.out.println(rounded); // 20.00Arithmetic Operations
BigDecimal is immutable: add(), subtract(), multiply(), divide() all return a new BigDecimal. add/subtract/multiply never lose precision and always succeed. divide() is different: division can produce a non-terminating decimal expansion (e.g. 1 divided by 3), and calling divide() without specifying a scale/rounding mode in such a case throws ArithmeticException rather than silently truncating or looping forever.
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal ten = new BigDecimal("10");
BigDecimal three = new BigDecimal("3");
try {
ten.divide(three); // ArithmeticException: non-terminating decimal expansion
} catch (ArithmeticException e) {
System.out.println("Caught: " + e.getMessage());
}
BigDecimal quotient = ten.divide(three, 2, RoundingMode.HALF_UP);
System.out.println(quotient); // 3.33Rounding Modes
java.math.RoundingMode defines named rounding strategies used by divide(), setScale(), and MathContext: HALF_UP (round half away from zero — the everyday-arithmetic default many people expect), HALF_EVEN (round half to the nearest even digit, minimizing cumulative bias across many roundings — also known as 'banker's rounding'), UP, DOWN, CEILING, FLOOR, and others. Choosing the correct rounding mode is often a business or regulatory requirement for financial software, not an arbitrary implementation detail.
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal v = new BigDecimal("2.5");
System.out.println(v.setScale(0, RoundingMode.HALF_UP)); // 3
System.out.println(v.setScale(0, RoundingMode.HALF_EVEN)); // 2 (rounds to the nearest even digit)
System.out.println(new BigDecimal("1.5").setScale(0, RoundingMode.HALF_EVEN)); // 2equals() Versus compareTo(): A Critical Distinction
This is the single most common BigDecimal bug: equals() considers both numeric value AND scale, so new BigDecimal("2.0").equals(new BigDecimal("2.00")) is false, even though they represent the same mathematical value. compareTo() compares purely by mathematical value, ignoring scale, so compareTo() returns 0 for those same two values. For numeric equality checks, compareTo() == 0 is almost always what's actually intended; equals() is appropriate only when scale itself is meant to be part of the comparison (e.g. checking that a value was formatted to exactly 2 decimal places).
Because equals() and hashCode() are scale-sensitive, using BigDecimal as a HashMap/HashSet key (or comparing amounts with ==/equals) is a frequent source of subtle bugs when values arrive with differing scales.
import java.math.BigDecimal;
BigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");
System.out.println(a.equals(b)); // false -- different scale
System.out.println(a.compareTo(b) == 0); // true -- same numeric value
System.out.println(a.hashCode() == b.hashCode()); // may differ, consistent with equals() being falseMathContext: Precision-Based Arithmetic
MathContext bundles together a target precision (total significant digits) and a RoundingMode, and can be passed to arithmetic operations to control the result's precision directly, as an alternative to controlling scale after the fact. MathContext.DECIMAL32/64/128 are predefined contexts approximating IEEE 754 decimal precision levels.
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
MathContext mc = new MathContext(5, RoundingMode.HALF_UP); // 5 significant digits
BigDecimal result = new BigDecimal("123.456789").round(mc);
System.out.println(result); // 123.46Monetary Calculations: A Practical Pattern
For money, a common convention is to store amounts as BigDecimal with a fixed, explicit scale (e.g. 2 for USD cents) applied consistently after every calculation, using a well-defined RoundingMode chosen deliberately (often HALF_UP for typical retail pricing, though the correct choice can be a legal/business requirement). Comparisons should use compareTo(), and any persisted/serialized representation should preserve the string form to avoid the double-constructor pitfall on reload.
import java.math.BigDecimal;
import java.math.RoundingMode;
public class InvoiceCalculator {
private static final int SCALE = 2;
static BigDecimal lineTotal(BigDecimal unitPrice, int quantity, BigDecimal taxRate) {
BigDecimal subtotal = unitPrice.multiply(BigDecimal.valueOf(quantity));
BigDecimal tax = subtotal.multiply(taxRate).setScale(SCALE, RoundingMode.HALF_UP);
return subtotal.add(tax).setScale(SCALE, RoundingMode.HALF_UP);
}
public static void main(String[] args) {
BigDecimal total = lineTotal(new BigDecimal("19.99"), 3, new BigDecimal("0.0825"));
System.out.println(total); // 64.92
}
}Common Mistakes Recap (Conceptual)
The bullets below are conceptual summaries; concrete wrong-vs-right code pairs for each appear in the Common Mistakes section of this topic: using new BigDecimal(double) instead of the String constructor or valueOf(); comparing BigDecimal values with equals() or == instead of compareTo(); calling divide() without a scale/rounding mode on values that don't divide evenly; and mixing BigDecimal with double in the same calculation, which reintroduces floating-point imprecision at the boundary.
Mental Model
Think of a BigDecimal as a plain decimal number written on paper — every digit before and after the decimal point exact, with the scale being literally 'how many digits are written after the point.' equals() checks whether the paper looks identical, digit for digit including trailing zeros; compareTo() checks whether the numbers they represent are mathematically the same, regardless of how they're written.
Key Takeaways
BigDecimal represents decimal values exactly (unscaled value x 10^-scale), avoiding the binary floating-point approximation errors inherent to double/float.
Always construct from a String (or BigDecimal.valueOf(double)) rather than new BigDecimal(double), which captures the double's exact (often surprising) binary value.
equals() considers scale as part of equality; compareTo() compares purely by numeric value. Use compareTo() == 0 for numeric equality checks in almost all cases.
divide() throws ArithmeticException on a non-terminating result unless you specify a scale and RoundingMode (or use a MathContext).
RoundingMode choice (HALF_UP, HALF_EVEN, etc.) can be a business or legal requirement for financial calculations — it is not an arbitrary detail.
BigDecimal is immutable; every arithmetic method returns a new instance rather than modifying the receiver.
BigDecimal price = new BigDecimal(19.99); // captures the double's exact binary approximation
BigDecimal price = new BigDecimal("19.99"); // or BigDecimal.valueOf(19.99) new BigDecimal(double) reflects the exact (often long, surprising) binary floating-point value already stored in the double, not the clean decimal the literal appears to show.
if (amount1.equals(amount2)) {... } // "2.0" and "2.00" are not equals() if (amount1.compareTo(amount2) == 0) {... } // compares purely by numeric value equals() treats different scales as different values, so mathematically equal amounts with differing trailing zeros compare unequal; compareTo() is the correct numeric-equality check.
BigDecimal result = ten.divide(three); // throws ArithmeticException: non-terminating decimal expansion
BigDecimal result = ten.divide(three, 2, RoundingMode.HALF_UP); // explicit scale and rounding
Division that doesn't terminate in decimal form has no exact BigDecimal result, so divide() requires an explicit scale and RoundingMode (or a MathContext) to know how to round.
double rate = 0.0825; BigDecimal tax = subtotal.multiply(BigDecimal.valueOf(rate * 100)).divide(BigDecimal.valueOf(100)); // reintroduces double imprecision
BigDecimal rate = new BigDecimal("0.0825"); BigDecimal tax = subtotal.multiply(rate);
Mixing double arithmetic into a BigDecimal calculation reintroduces the floating-point approximation errors BigDecimal exists to avoid; keep the entire calculation in BigDecimal from the start.
Why is new BigDecimal(0.1) not equal to new BigDecimal("0.1")?
new BigDecimal(double) captures the exact binary floating-point value already stored in the double (which is only an approximation of 0.1), while new BigDecimal(String) parses the exact decimal text "0.1" directly.
What is BigDecimal's scale, and how do "1.5" and "1.50" differ in scale?
Scale is the number of digits to the right of the decimal point; "1.5" has scale 1 and "1.50" has scale 2 — they represent the same numeric value but with a different explicit scale, and are NOT equals()-equal because of it.
Why does BigDecimal's divide() sometimes throw ArithmeticException?
Division that produces a non-terminating decimal expansion (e.g. 10 divided by 3) has no exact finite BigDecimal representation, so divide() without an explicit scale/rounding mode throws rather than guessing how to truncate or round.
When should you use compareTo() instead of equals() for BigDecimal?
Whenever you want purely numeric equality regardless of scale — almost always the actual intent — since equals() additionally requires matching scale, which can make mathematically identical values compare as unequal.
What is the difference between BigDecimal.valueOf(double) and new BigDecimal(double)?
valueOf() converts the double through Double.toString() first, yielding the decimal value the double's canonical string form shows (usually matching programmer intent); the constructor captures the double's raw exact binary value directly, which is often a long, surprising decimal expansion.
Why is HALF_EVEN rounding sometimes called 'banker's rounding'?
It rounds a half-value to whichever adjacent digit is even, which, applied consistently across many roundings, statistically minimizes cumulative rounding bias compared to always rounding half-values the same direction — a property useful in financial aggregation.
Why is BigDecimal generally preferred over double for monetary calculations?
BigDecimal represents decimal values exactly, with explicit control over scale and rounding, while double's binary floating-point representation introduces small approximation errors that compound across many operations and can misrepresent exact cent amounts.
Write a program that adds three prices (given as strings) using BigDecimal and prints the total rounded to 2 decimal places using HALF_UP.
import java.math.BigDecimal; import java.math.RoundingMode; public class PriceSum { public static void main(String[] args) { BigDecimal a = new BigDecimal("12.995"); BigDecimal b = new BigDecimal("4.10"); BigDecimal c = new BigDecimal("0.005"); BigDecimal total = a.add(b).add(c).setScale(2, RoundingMode.HALF_UP); System.out.println(total); } }
The three BigDecimal values are added exactly (17.1), then setScale(2, HALF_UP) rounds the result to two decimal places, printing 17.10.
What does this print?
false, then 0 — equals() returns false because the two values have different scale (1 vs 2), while compareTo() returns 0 because they represent the same numeric value.
Write a program that safely divides two BigDecimal values, catching ArithmeticException if the division does not terminate, and retrying with an explicit scale.
import java.math.BigDecimal; import java.math.RoundingMode; public class SafeDivide { static BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator) { try { return numerator.divide(denominator); } catch (ArithmeticException e) { return numerator.divide(denominator, 10, RoundingMode.HALF_UP); } } public static void main(String[] args) { System.out.println(safeDivide(new BigDecimal("10"), new BigDecimal("2"))); // 5 System.out.println(safeDivide(new BigDecimal("10"), new BigDecimal("3"))); // 3.3333333333 } }
10/2 terminates exactly so the plain divide() succeeds and returns 5; 10/3 does not terminate, throwing ArithmeticException, which is caught and retried with an explicit scale of 10 and HALF_UP rounding.
BigDecimal a = new BigDecimal("3.0");
BigDecimal b = new BigDecimal("3.00");
System.out.println(a.equals(b));
System.out.println(a.compareTo(b));