If you have written Java for any length of time, you have probably been burned by this:
System.out.println(0.1 + 0.2);
0.30000000000000004
That trailing 4 is not a Java bug. It is the single most misunderstood fact in everyday programming, and getting it wrong has cost real companies real money. This guide starts from that one surprising line and walks all the way to production-grade decisions: when plain double is perfectly fine, when BigDecimal is mandatory, and when a humble long is the fastest correct answer of all.
Every code block below was compiled and run on a real JDK, and the output shown is the actual output — not what should happen, but what does.
The bug that looks like magic
Let’s make the abstract concrete. Imagine a checkout that adds ten items at 10 cents each:
double price = 0.10;
double total = 0.0;
for (int i = 0; i < 10; i++) {
total += price;
}
System.out.println("Total: " + total);
System.out.println("Is it exactly 1.0? " + (total == 1.0));
Total: 0.9999999999999999
Is it exactly 1.0? false
Ten dimes did not add up to a dollar. If a downstream check says if (total == 1.0), it fails. If you round for display but compare on the raw value, your invoice and your ledger disagree by a fraction of a cent — and at scale, fractions of a cent become audit findings.
The fix is not "add a tiny tolerance everywhere." The fix is understanding why this happens, because the why tells you exactly which tool to reach for.
Why it happens: computers count in binary
Humans write numbers in base 10. A double stores them in base 2. The conversion between the two is where precision leaks away.
In base 10, some fractions never terminate — 1/3 is 0.3333… forever. In base 2, far more fractions fail to terminate, and 0.1 is one of them. The number 0.1 simply has no exact finite binary representation, the same way 1/3 has no exact finite decimal one. The CPU stores the closest 64-bit value it can, and that value is not quite 0.1.
You can see the truth with BigDecimal, which can print the exact value a double is holding:
import java.math.BigDecimal;
System.out.println(new BigDecimal(0.1));
0.1000000000000000055511151231257827021181583404541015625
That long tail is what double actually stored when you typed 0.1. Every arithmetic operation carries that tiny error forward, and occasionally — like with ten dimes — the errors line up and become visible.
What a double looks like inside
A Java double is an IEEE 754 64-bit number with three parts: a sign bit, an 11-bit exponent, and a 52-bit fraction (the mantissa). Those 52 fraction bits are the entire budget of precision — roughly 15–17 significant decimal digits. Anything that needs more, or needs exactness in base 10, will not fit.
The key takeaway from that diagram: precision is finite and fixed. A double is a base-2 approximation living in 64 bits, so base-10 values like 0.1, 0.2, or 19.99 are almost never stored exactly.
float is the same problem, only worse
float is the 32-bit version — 23 fraction bits instead of 52. If double makes you nervous, float should make you run:
float f = 0.1f;
System.out.println(new BigDecimal(f));
0.100000001490116119384765625
The error is visible after only seven digits. Rule of thumb: never use float for anything you care about being correct, and never, ever for money.
When double is actually the right choice
Here is the part most "always use BigDecimal" advice gets wrong: most numbers in your program are not money, and for them double is the correct, fast, idiomatic choice.
Use double when the quantity is inherently approximate and a rounding error in the 16th digit is meaningless:
- Physics, geometry, graphics, distances, coordinates.
- Machine-learning features, statistics, averages, scientific measurements.
- Sensor readings, percentages, ratios, anything already noisy.
For these, double is faster, uses less memory, and is supported directly by the CPU. Reaching for BigDecimal here is not "safer" — it is slower code solving a problem you do not have. The rule is about intent: double is for measuring, not for counting exact units.
When you do compare doubles, compare with a tolerance rather than ==:
double a = 0.1 + 0.2;
double epsilon = 1e-9;
System.out.println(Math.abs(a - 0.3) < epsilon); // true
BigDecimal: the correct tool for money — and its three traps
When you are counting exact decimal units — currency, tax, interest, anything that must reconcile to the penny — you need a type that thinks in base 10. That is BigDecimal. It stores an arbitrary-precision integer (the unscaled value) plus a scale (how many digits sit after the decimal point), so 19.99 is stored exactly as 1999 × 10⁻².
But BigDecimal has three traps that catch nearly every newcomer. Learn them once and you will never debug them at 2 a.m.
Trap 1: never construct from a double
This is the mistake that quietly defeats the entire point of using BigDecimal:
System.out.println(new BigDecimal(0.1)); // from double
System.out.println(new BigDecimal("0.1")); // from String
0.1000000000000000055511151231257827021181583404541015625
0.1
new BigDecimal(0.1) faithfully copies the already-broken double. You laundered a floating-point error into a "precise" type. Always build BigDecimal from a String (or from BigDecimal.valueOf(double), which routes through the string form). Once values are clean, arithmetic is exact:
BigDecimal a = new BigDecimal("0.10");
BigDecimal b = new BigDecimal("0.20");
System.out.println(a.add(b));
0.30
Trap 2: equals() compares scale, compareTo() compares value
Two BigDecimals that represent the same amount are not equal if their scales differ:
BigDecimal x = new BigDecimal("1.0");
BigDecimal y = new BigDecimal("1.00");
System.out.println(x.equals(y)); // false — scale 1 vs scale 2
System.out.println(x.compareTo(y) == 0); // true — same numeric value
false
true
For "is this the same amount?", always use compareTo() == 0. This trap also means you should never use BigDecimal as a HashMap key or in a HashSet unless you have normalized the scale first, because equals() and hashCode() both factor in scale.
Trap 3: division can throw — give it a scale and a rounding mode
A division with no exact decimal result throws at runtime:
new BigDecimal("1").divide(new BigDecimal("3"));
java.lang.ArithmeticException: Non-terminating decimal expansion;
no exact representable decimal result.
BigDecimal refuses to silently guess. You must tell it how precise and how to round:
import java.math.RoundingMode;
BigDecimal r = new BigDecimal("1")
.divide(new BigDecimal("3"), 10, RoundingMode.HALF_EVEN);
System.out.println(r);
0.3333333333
That RoundingMode argument is not boilerplate — it is a decision, and the next section explains why.
Rounding modes: the default will surprise you
Ask a room of developers what 2.5 rounds to and everyone says 3. Ask what BigDecimal does by default and the room goes quiet — because the answer depends on the mode:
System.out.println(new BigDecimal("2.5").setScale(0, RoundingMode.HALF_UP)); // 3
System.out.println(new BigDecimal("2.5").setScale(0, RoundingMode.HALF_EVEN)); // 2
System.out.println(new BigDecimal("3.5").setScale(0, RoundingMode.HALF_EVEN)); // 4
3
2
4
HALF_UP is "round half away from zero" — the schoolbook rule. HALF_EVEN — banker’s rounding — rounds a halfway value to the nearest even digit, so 2.5 → 2 but 3.5 → 4. Why? Over many roundings, always rounding .5 up introduces a consistent upward bias; banker’s rounding cancels it out, which is exactly what financial and statistical systems want. HALF_EVEN is the default for IEEE 754 floating-point and for MathContext.DECIMAL*.
Pick your rounding mode deliberately. "Whichever was the default" is how a billing system slowly drifts a few cents in the house’s favor and earns a regulator’s attention.
scale, precision, and MathContext (reference grade)
Two concepts govern every BigDecimal:
- scale — the number of digits to the right of the decimal point.
19.99has scale 2. Money math is usually fixed-scale: set scale 2 (or 4 for sub-cent interest) and a rounding mode, and keep it consistent everywhere. - precision — the total number of significant digits. Controlled with a
MathContext, which bundles a precision with a rounding mode and is what you want for scientific-style arbitrary-precision work.
import java.math.MathContext;
MathContext mc = new MathContext(5, RoundingMode.HALF_EVEN);
System.out.println(new BigDecimal("2").divide(new BigDecimal("3"), mc));
0.66667
Rule of thumb: for money, think in scale (fixed decimal places); for science, think in precision (significant figures via MathContext).
Fixed-point: store cents as a long
There is a third option that is faster than BigDecimal and, for plain currency, often simpler: don’t store dollars at all — store the smallest unit (cents, or "minor units") as a long integer. Integers in base 2 are exact, so the original ten-dimes bug simply cannot happen:
long priceCents = 10; // $0.10
long totalCents = 0;
for (int i = 0; i < 10; i++) {
totalCents += priceCents;
}
System.out.println(totalCents + " cents = $" + (totalCents / 100.0));
System.out.println("Exact? " + (totalCents == 100));
100 cents = $1.0
Exact? true
This is the "minor units" model used by Stripe’s API, by many ledger systems, and by anyone who needs raw speed with exact whole-unit math. The trade-offs: you manage the decimal point yourself, you must watch for long overflow on very large sums (a long caps around 9.2 quintillion — still €92 quadrillion in cents), and anything requiring division with fractional results (interest, currency conversion, tax splits) pushes you back toward BigDecimal. For addition, subtraction, and counting, fixed-point long is hard to beat.
What precision actually costs
Correctness has a price, and it helps to know its size. The loop below adds the same value 20 million times using each approach (warmed up, single JVM run — a rough microbenchmark, not a rigorous JMH study):
long (cents) : ~0 ms (JIT folds the integer loop to nearly free)
double : ~35 ms
BigDecimal : ~153 ms
So on this add-heavy workload, BigDecimal ran roughly 4–5× slower than double, and primitive long/double arithmetic is effectively free because the CPU and JIT handle it natively. BigDecimal allocates a new immutable object per operation, which adds CPU and garbage-collection pressure.
Read that the right way. For a request that does a handful of money calculations, 4–5× slower than "instant" is still instant — correctness wins, use BigDecimal without a second thought. The cost only matters in tight inner loops doing millions of operations, and that is precisely where fixed-point long (if the math allows) or double (if approximation is acceptable) earns its place. Never microbenchmark in production code; reach for JMH when numbers actually drive a decision.
The decision, in one table
Every section so far collapses into a single question — what am I really storing? — and one table:
| Your data | Use | Why |
|---|---|---|
| Money, tax, billing, anything that must reconcile exactly | BigDecimal (built from String, fixed scale, explicit RoundingMode) |
Exact base-10 arithmetic; correctness over speed |
| Currency with only +, −, ×, and you want max speed | long of minor units (cents) |
Integers are exact and fastest; you own the decimal point |
| Measurements, science, graphics, ML, statistics | double |
Inherently approximate; native CPU speed; compare with epsilon |
| Anything at all | not float |
7 digits of precision is a trap waiting to spring |
If you remember only one sentence: count exact things with BigDecimal or long, measure approximate things with double, and never store money in float or double.
Going further (advanced references)
Once the fundamentals are solid, these are the tools and topics worth knowing:
- Money & Currency API (JSR 354 / Moneta). A real
MonetaryAmounttype with currency units, formatting, and exchange-rate operations, so you stop passing rawBigDecimals and accidentally adding dollars to euros. The reference implementation is Moneta. - decimal4j. A library offering fixed-point decimal arithmetic backed by
long, givingBigDecimal-style fixed-scale correctness at speeds close to primitives — the best of both worlds for high-throughput financial code. - Model money as a type, not a primitive. A small
record Money(long minorUnits, Currency currency)(Java 16+) centralizes rounding and currency rules in one place and makes illegal states unrepresentable, instead of scatteringBigDecimalmath across the codebase. - Persistence. Map money columns to SQL
DECIMAL/NUMERIC, neverFLOAT/DOUBLE. In JPA/Hibernate, persistBigDecimalwith an explicitprecisionandscaleon the column so the database enforces them too. - JSON. Jackson can silently widen numbers to
doubleduring deserialization. EnableDeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS(or bind toBigDecimalfields) so a price never round-trips through adoubleon its way in. - The IEEE 754 standard itself, if you want the ground truth on subnormals, infinities, NaN, and rounding — the foundation under every
doubleyou will ever write.
The thread connecting all of it is the very first line of this post. 0.1 + 0.2 is not equal to 0.3 because a double is a base-2 approximation. Once that clicks, every other decision — BigDecimal for exactness, long for speed, double for measurement, String constructors, banker’s rounding, fixed scales — stops being a rule to memorize and becomes a consequence you can derive. That is the difference between copying advice and understanding it.
FAQ
Why not use double for money?
A double is a base-2 (binary) approximation, so common base-10 amounts like 0.10 or 19.99 are not stored exactly. The tiny errors accumulate — ten dimes added as double come to 0.9999999999999999, not 1.0 — and money has to reconcile to the exact penny. Use BigDecimal or long cents instead, and never float.
Is BigDecimal always slower?
It is slower than primitives — roughly 4–5× slower than double in an add-heavy loop — because each operation allocates a new immutable object and adds garbage-collection pressure. But “slower” is relative: a request doing a handful of money calculations will not notice it, and correctness wins easily. The cost only matters in tight inner loops running millions of operations.
Should I use BigDecimal or long cents?
Use long minor units (cents) when your math is mostly addition, subtraction, and multiplication and you want maximum speed with exact whole-unit results — the model Stripe and many ledgers use. Switch to BigDecimal when you need division with fractional results (interest, currency conversion, tax splits), variable scale, or built-in rounding control. Both are correct; long is faster, BigDecimal is more flexible.
What is the difference between precision and scale?
Precision is the total number of significant digits in the number; scale is how many of those digits sit to the right of the decimal point. 19.99 has precision 4 and scale 2. For money you fix the scale (e.g. 2) and a rounding mode; for scientific work you control precision through a MathContext.
Why does BigDecimal.equals() return false?
Because equals() compares both value and scale, so new BigDecimal("1.0") and new BigDecimal("1.00") are not equal even though they represent the same amount. To compare numeric value only, use compareTo() == 0. This is also why a BigDecimal makes a risky HashMap key unless you normalize the scale first.
Sources & credits: IEEE 754 double-precision diagram by Codekaizen via Wikimedia Commons (CC BY-SA 4.0). All code samples were compiled and executed on OpenJDK; the output shown is the real program output.