Topic 1.4
Decimals: double, float and Why 0.1 + 0.2 Isn't 0.3
In one line
double and float store numbers in binary scientific notation with a fixed number of digits, so most decimal fractions like 0.1 are stored as the nearest binary approximation. That's why 0.1 + 0.2 prints 0.30000000000000004, why you never compare decimals with ==, and why money belongs in BigDecimal.
Think of it like this
Write one third as a decimal on a sticky note with room for only 8 digits. You get 0.33333333, close but not exact, and three of those notes add up to 0.99999999, not 1. Computers have the same problem, but in binary, where even one tenth can't be written exactly. Every double is a sticky note with room for about 16 significant decimal digits.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Floating point
- A way to store decimals where the decimal point can 'float': a fixed number of digits plus a power that says where the point goes, like 6.02 × 10²³.
- IEEE 754
- The international standard for floating-point numbers that Java, your CPU and almost every language follow.
- Exponent
- The power part of scientific notation. It says how far to move the point.
- Precision
- How many significant digits a type can keep: about 7 for
float, about 15 to 16 fordouble. - Rounding error
- The tiny difference between the true answer and the nearest value the type can store.
- NaN
- 'Not a Number': a special
doublevalue for undefined results like0.0 / 0orMath.sqrt(-1). - Infinity
- A special
doublevalue for results too big to store, or a non-zero number divided by zero. - Tolerance (epsilon)
- A small allowed difference. Two decimals 'are equal' if they differ by less than it.
- BigDecimal
- A class in
java.maththat stores decimal numbers exactly, digit by digit. Used for money. - Scale
- In a
BigDecimal, the number of digits after the decimal point.19.99has scale 2.
Step by step
01Why some fractions can never be exact
In decimal, a fraction is exact only if its denominator is built from 2s and 5s (the factors of 10): 1/4 = 0.25 is exact, 1/3 = 0.333… is not.
In binary, only denominators that are powers of 2 are exact: 1/2, 1/4, 3/8, 0.75 are perfect. 1/10 has a factor of 5, so in binary it repeats forever: 0.0001100110011… The computer has to cut it off somewhere.
02How a double is laid out
A double is 64 bits split into three parts. The sign (1 bit) says positive or negative. The exponent (11 bits) says which power of 2 to multiply by. The fraction (52 bits) holds the digits after an implied leading 1.
So a double is really ±1.xxxx…(52 binary digits) × 2^e. 52 binary digits are worth about 15.95 decimal digits, which is where 'about 16 significant digits' comes from. A float has 1 + 8 + 23 bits: about 7 digits.
03What 0.1 really is
The double nearest to 0.1 is exactly 0.1000000000000000055511151231257827021181583404541015625. You can see it with new BigDecimal(0.1), which shows a double's exact value.
When you print a double, Java doesn't print that long value. Double.toString prints the shortest decimal string that would read back as the same double, so 0.1 prints as 0.1, which hides the error until arithmetic exposes it.
0.1 + 0.2: both inputs are slightly too big, their sum rounds to a double that is not the double nearest 0.3, and the shortest string for that double is 0.30000000000000004.
04Comparing decimals safely
Replace a == b with Math.abs(a - b) < EPS where EPS is a tolerance that fits the problem: 1e-9 for numbers around 1, bigger for big numbers. A fixed tolerance fails for huge values, so robust code uses Math.abs(a - b) <= 1e-9 * Math.max(Math.abs(a), Math.abs(b)).
Also beware of loops driven by decimals: for (double d = 0; d != 1.0; d += 0.1) never stops, because d goes from 0.9999999999999999 straight past 1.0. Loop over integers and compute the decimal: for (int i = 0; i <= 10; i++) { double d = i / 10.0; }.
05Infinity, NaN and negative zero
IEEE 754 defines special values so that a calculation can continue instead of crashing. 1.0 / 0 is Infinity. 0.0 / 0, Infinity - Infinity and Math.sqrt(-1) are NaN.
NaN poisons everything it touches: NaN + 1 is NaN, and every comparison with NaN is false, including NaN == NaN. Check with Double.isNaN(x); check for both special cases with Double.isFinite(x).
There is also -0.0. It compares equal to 0.0 with ==, but 1 / -0.0 is -Infinity. And beware the name Double.MIN_VALUE: it's the smallest positive double (4.9E-324), not the most negative one; that's -Double.MAX_VALUE.
06Precision runs out for big numbers
Because a double keeps only 53 significant bits, the gap between neighbouring doubles grows with their size. Near 1 the gap is about 2.2 × 10⁻¹⁶ (Math.ulp(1.0)). Near 10¹⁶ the gap is 2, so adding 1 does nothing.
A float hits this much earlier: 16_777_216f + 1 is still 16,777,216. Using float for a running total of many values (like counting pixels or summing sales) silently stops counting.
07Money: BigDecimal or long cents
BigDecimal stores an unscaled integer plus a scale: 19.99 is 1999 with scale 2. Arithmetic is exact. Division and rounding need you to state a RoundingMode (for example HALF_UP like school maths, or HALF_EVEN, 'banker's rounding', which is unbiased).
Always build it from a String (new BigDecimal("0.1")) or with BigDecimal.valueOf(0.1), which goes through the double's shortest string. new BigDecimal(0.1) copies the double's exact binary value, error included.
Compare BigDecimals with compareTo, not equals: new BigDecimal("2.0").equals(new BigDecimal("2.00")) is false because the scales differ, while compareTo returns 0.
Try it yourself
- 1
Hunt for exact decimals
In the first example, try
0.5 + 0.25 == 0.75and0.1 + 0.7. Predict which one is exact before running. (Hint: think about which denominators are powers of 2.) - 2
Look at a double's real value
Print
new BigDecimal(0.5),new BigDecimal(0.3)andBigDecimal.valueOf(0.3). The first is exact, the second shows the hidden error, the third prints0.3because it goes throughDouble.toString. - 3
Change the rounding
In the money example, change
RoundingMode.HALF_UPtoRoundingMode.UPand thenRoundingMode.DOWN. Predict the tax each time (10.80 and 10.79).
Code & diagrams
Expected output
0.1 + 0.2 = 0.30000000000000004
equals 0.3? false
1.03 - 0.42 = 0.6100000000000001
3 * 0.1 = 0.30000000000000004
ten 0.1s = 0.9999999999999999
close to 1.0? true
exact 0.1 = 0.1000000000000000055511151231257827021181583404541015625Expected output
1.0 / 0 = Infinity
-1.0 / 0 = -Infinity
0.0 / 0 = NaN
NaN == NaN : false
isNaN : true
inf - inf = NaN
sqrt(-1) = NaN
-0.0 == 0.0 : true
1 / -0.0 = -Infinity
MIN_VALUE = 4.9E-324
float 2^24 + 1 = 1.6777216E759.97 × 0.18 = 10.7946, which HALF_UP rounds to 10.79.
Expected output
double: 0.6100000000000001
BigDecimal: 0.61
subtotal: 59.97
tax 18%: 10.79
total: 70.76
equals: false
compareTo: 0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Loop until a double equals 1.0
Write for (double d = 0.0; d != 1.0; d += 0.1) System.out.println(d); and run it.
Break #2
Build a BigDecimal from a double
Write System.out.println(new BigDecimal(0.1)); expecting 0.1.
Myth vs fact
Myth
0.1 + 0.2 != 0.3 is a Java bug.
Fact
It's how IEEE 754 binary floating point works in every language that uses it: JavaScript, Python, C, Go all print 0.30000000000000004.
Myth
float is faster than double, so use it.
Fact
On modern 64-bit CPUs scalar float and double arithmetic run at about the same speed. float only wins when memory or SIMD width matters, as in huge arrays.
Myth
Dividing by zero always crashes.
Fact
Only integer division does. Floating-point division by zero gives Infinity, -Infinity or NaN and the program keeps running.
Myth
Double.MIN_VALUE is the most negative double.
Fact
It's the smallest positive double, 4.9E-324. The most negative finite double is -Double.MAX_VALUE.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Since Java 17 (JEP 306) all floating-point arithmetic is strict IEEE 754 everywhere, so the
strictfpkeyword is obsolete and javac warns about it. Before that, x87-era JVMs could use extended precision for intermediates, giving slightly different results on different machines. - ▸
Java 19 rewrote
Double.toStringandFloat.toString(JDK-4511638) to always print the shortest decimal that round-trips. Older JDKs occasionally printed an extra digit, for example2.0E23printed as2.0000000000000002E23before Java 19. Code that parses or compares printed doubles across JDK versions can notice. - ▸
Floating-point addition is not associative:
(0.1 + 0.2) + 0.3is0.6000000000000001while0.1 + (0.2 + 0.3)is0.6. That's why parallel stream sums of doubles can differ run to run (Topic 10.8) and why the JIT is not allowed to reorder them. - ▸
For summing many doubles accurately,
DoubleStream.sum()uses Kahan (compensated) summation internally, which reduces accumulated rounding error compared with a plain loop.
Remember this
- 1
Java's
floatanddoublefollow the IEEE 754 standard, the same format your CPU uses. Adoublehas 64 bits: 1 sign bit, 11 exponent bits and 52 fraction bits. It works like scientific notation in base 2: value = ±1.fraction × 2^exponent. - 2
Most decimal fractions are repeating in binary, just as 1/3 repeats in decimal. 0.1 in binary is 0.000110011001100… forever, so it's rounded to the nearest representable value, which is slightly more than 0.1. Each operation then rounds again. Small errors are normal and expected; they are not bugs in Java.
- 3
**Never compare decimals with
==unless you know the values are exact (like 0.5 or 2.0). Compare with a tolerance**:Math.abs(a - b) < 1e-9. For values of very different sizes, scale the tolerance with the numbers (a relative tolerance). - 4
Floating-point division by zero doesn't throw:
1.0 / 0isInfinity,-1.0 / 0is-Infinityand0.0 / 0is NaN ('Not a Number'). NaN is not equal to anything, not even itself, so test it withDouble.isNaN(x). Integer division by zero, by contrast, throws (Topic 1.7). - 5
**Money must not use
double**. Usejava.math.BigDecimalcreated from a String (new BigDecimal("19.99")), or store whole minor units (paise, cents) in along.BigDecimaldoes exact decimal arithmetic and lets you choose how to round withRoundingMode. - 6
doubleholds every whole number exactly up to 2⁵³ = 9,007,199,254,740,992; past that, gaps appear. Afloatis exact only up to 2²⁴ = 16,777,216, and keeps about 7 significant digits. Preferdoubleeverywhere unless memory in a huge array truly matters.
Explain it without notes
Explain why 0.1 + 0.2 prints 0.30000000000000004 in Java.
How should you compare two doubles for equality, and why does a fixed epsilon sometimes fail?
What are NaN and Infinity, how do you produce them, and how do you test for them?
Why shouldn't you store money in a double, and what should you use instead?
Practice
Add 0.1 ten times. Print the sum, whether it == 1.0, and whether it is within 1e-9 of 1.0.
With BigDecimal, compute the total for 3 items at 19.99 plus 18% tax rounded to 2 decimals (HALF_UP).
Show that double can't count past 2⁵³: start from 9007199254740992.0, add 1, and print whether the value changed.
Trade-offs
- ↔
doubleis fast, hardware-supported and fine for measurements, physics and statistics, where inputs are approximate anyway; it's wrong for money and exact counting. - ↔
BigDecimalis exact and gives full control over rounding, but it's slower, allocates objects, and needs method calls instead of operators. - ↔
Storing money as
longminor units is fast and exact, but you must handle currency scale (some currencies have 0 or 3 decimal places) and convert carefully for display and division.
Done when you can
I can explain why most decimal fractions can't be stored exactly in binary.
I compare doubles with a tolerance, never with
==.I know how to produce and test for
Infinity,NaNand-0.0.I use
BigDecimalfrom Strings (or long cents) for money, with an explicitRoundingMode.I know a
doubleis exact for integers only up to 2⁵³ and afloatup to 2²⁴.