Command Palette

Search for a command to run...

PHASE 1Beginner ~27 min· topic 4 of 14

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 for double.
Rounding error
The tiny difference between the true answer and the nearest value the type can store.
NaN
'Not a Number': a special double value for undefined results like 0.0 / 0 or Math.sqrt(-1).
Infinity
A special double value 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.math that stores decimal numbers exactly, digit by digit. Used for money.
Scale
In a BigDecimal, the number of digits after the decimal point. 19.99 has 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.

How a double is laid outdiagram
Rendering diagram…

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.

What 0.1 really isdiagram
Rendering diagram…

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. 1

    Hunt for exact decimals

    In the first example, try 0.5 + 0.25 == 0.75 and 0.1 + 0.7. Predict which one is exact before running. (Hint: think about which denominators are powers of 2.)

  2. 2

    Look at a double's real value

    Print new BigDecimal(0.5), new BigDecimal(0.3) and BigDecimal.valueOf(0.3). The first is exact, the second shows the hidden error, the third prints 0.3 because it goes through Double.toString.

  3. 3

    Change the rounding

    In the money example, change RoundingMode.HALF_UP to RoundingMode.UP and then RoundingMode.DOWN. Predict the tax each time (10.80 and 10.79).

Code & diagrams

Rounding errors you can see New tab
Sign in to run this example in your browser.

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.1000000000000000055511151231257827021181583404541015625
Infinity, NaN and other special values New tab
Sign in to run this example in your browser.

Expected 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.6777216E7
Money done right with BigDecimal New tab

59.97 × 0.18 = 10.7946, which HALF_UP rounds to 10.79.

Sign in to run this example in your browser.

Expected output

double: 0.6100000000000001
BigDecimal: 0.61
subtotal: 59.97
tax 18%:  10.79
total:    70.76
equals:    false
compareTo: 0

Break 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.

terminal
$ javac Main.java
java Main
── what you'll see ──
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
... (never stops; press Ctrl+C)

Break #2

Build a BigDecimal from a double

Write System.out.println(new BigDecimal(0.1)); expecting 0.1.

terminal
$ javac Main.java
java Main
── what you'll see ──
0.1000000000000000055511151231257827021181583404541015625

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 strictfp keyword 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.toString and Float.toString (JDK-4511638) to always print the shortest decimal that round-trips. Older JDKs occasionally printed an extra digit, for example 2.0E23 printed as 2.0000000000000002E23 before 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.3 is 0.6000000000000001 while 0.1 + (0.2 + 0.3) is 0.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. 1

    Java's float and double follow the IEEE 754 standard, the same format your CPU uses. A double has 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. 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. 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. 4

    Floating-point division by zero doesn't throw: 1.0 / 0 is Infinity, -1.0 / 0 is -Infinity and 0.0 / 0 is NaN ('Not a Number'). NaN is not equal to anything, not even itself, so test it with Double.isNaN(x). Integer division by zero, by contrast, throws (Topic 1.7).

  5. 5

    **Money must not use double**. Use java.math.BigDecimal created from a String (new BigDecimal("19.99")), or store whole minor units (paise, cents) in a long. BigDecimal does exact decimal arithmetic and lets you choose how to round with RoundingMode.

  6. 6

    double holds every whole number exactly up to 2⁵³ = 9,007,199,254,740,992; past that, gaps appear. A float is exact only up to 2²⁴ = 16,777,216, and keeps about 7 significant digits. Prefer double everywhere unless memory in a huge array truly matters.

Explain it without notes

01

Explain why 0.1 + 0.2 prints 0.30000000000000004 in Java.

02

How should you compare two doubles for equality, and why does a fixed epsilon sometimes fail?

03

What are NaN and Infinity, how do you produce them, and how do you test for them?

04

Why shouldn't you store money in a double, and what should you use instead?

Practice

01

Add 0.1 ten times. Print the sum, whether it == 1.0, and whether it is within 1e-9 of 1.0.

02

With BigDecimal, compute the total for 3 items at 19.99 plus 18% tax rounded to 2 decimals (HALF_UP).

03

Show that double can't count past 2⁵³: start from 9007199254740992.0, add 1, and print whether the value changed.

Trade-offs

  • ↔

    double is fast, hardware-supported and fine for measurements, physics and statistics, where inputs are approximate anyway; it's wrong for money and exact counting.

  • ↔

    BigDecimal is exact and gives full control over rounding, but it's slower, allocates objects, and needs method calls instead of operators.

  • ↔

    Storing money as long minor 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, NaN and -0.0.

  • I use BigDecimal from Strings (or long cents) for money, with an explicit RoundingMode.

  • I know a double is exact for integers only up to 2⁵³ and a float up to 2²⁴.