skip to content
everything.basics.floatingPoint

floating point

why 0.1 + 0.2 != 0.3, IEEE 754 bit by bit, and when to reach for BigDecimal.

chapter
chapter 02.04
reading time
9 min read

a double stores a number as binary scientific notation: a sign, an exponent and 52 bits of digits. most decimals, like 0.1, have no exact binary form, so what's stored is the nearest double. that's the whole reason 0.1 + 0.2 != 0.3. compare doubles with a tolerance, and use BigDecimal for money.

the picture

try writing 1/3 as a decimal. 0.3333... and you run out of paper eventually, so you round. binary has the exact same problem with different numbers: 1/10 in binary is 0.0001100110011... repeating forever. a double has room for 52 bits of digits, so it cuts off and rounds to the nearest value it can represent.

think of a ruler. a double can only point at tick marks. 0.1 falls between two ticks, so java quietly uses the closest tick. most of the time the error is invisible. add a couple of those slightly-off numbers together and the errors can push the result onto a different tick than the one you'd get by typing the answer directly.

0.1 + 0.2

java =
0.1stored as 0.10000000000000000555111512312578270211815834045410156250.2stored as 0.200000000000000011102230246251565404236316680908203125exact sum0.3000000000000000166533453693773481063544750213623046875 (not a double)0.1 + 0.2rounds to 0.3000000000000000444089209850062616169452667236328125 → prints 0.300000000000000040.3stored as 0.299999999999999988897769753748434595763683319091796875

the exact answer falls between two doubles, so it's rounded to the nearest one. that's the one just above the double you get when you type 0.3, so 0.1 + 0.2 == 0.3 is false.

pick an expression. the digits are exact (the same ones new BigDecimal(x) prints), and the amber part is where the stored value stops matching what you typed. on the number line, the diamond is the true answer, the dot is the double it rounds to.

see? neither 0.1 nor 0.2 nor 0.3 is stored exactly. the exact sum of the stored 0.1 and the stored 0.2 lands between two doubles, gets rounded to the upper one, and that one isn't the double you get when you type 0.3. one tick apart, about 5.5 × 10⁻¹⁷. enough for == to say no.

you can see the stored values yourself. new BigDecimal(double) prints the exact value, every digit:

Main.java
import java.math.BigDecimal;
 
void main() {
    IO.println(0.1 + 0.2);
    IO.println(0.1 + 0.2 == 0.3);
    IO.println(new BigDecimal(0.1));
    IO.println(new BigDecimal(0.3));
}
output
0.30000000000000004
false
0.1000000000000000055511151231257827021181583404541015625
0.299999999999999988897769753748434595763683319091796875

and why does println(0.1) show 0.1 then? because java prints the shortest decimal that still identifies that exact double. 0.1 is enough to pick out the stored value, so that's what you see. it's a polite lie. 0.30000000000000004 is what it takes to identify the sum.

how the bits are laid out

a double is 64 bits: 1 sign bit, 11 exponent bits, 52 mantissa bits. a float is the same idea in 32: 1, 8 and 23. the value is roughly ±1.mantissa × 2exponent, in binary. the exponent says where the point goes, the mantissa holds the digits.

float0x3dcccccd
sign · 1 bitexponent · 8 bitsmantissa · 23 bits
actually stored: 0.10000000149…
a real float, bit by bit: sign, exponent, mantissa. type any number (try 0.1, 1, 0.5, 0.75, 16777217) or click bits. "actually stored" is the value those bits really hold.

numbers that are sums of powers of two (0.5, 0.75, 0.25, 3.0) fit exactly. try 0.75: the mantissa is just two bits. anything with a factor of 5 or 3 in the denominator (0.1, 0.2, 1/3) repeats forever in binary and gets rounded.

float gives you about 7 significant decimal digits, double about 15 to 17. use double. a float isn't more exact, it just prints fewer digits: 0.1f + 0.2f prints 0.3, because there are fewer floats near 0.3 to tell apart.

the gaps grow with the number

the ticks on the ruler aren't evenly spaced. there are as many doubles between 1 and 2 as between 1,000,000 and 2,000,000, so the gap between neighbours (the ulp, unit in the last place) grows with the size of the number:

the gap between a double and the next one up, from Math.ulp(x) on jdk 25. log scale: each bar is a different universe. past 253 (about 9 × 1015) the gap is bigger than 1, and doubles can't even count.
Main.java
void main() {
    IO.println(Math.ulp(1.0) + " " + Math.ulp(1e6) + " " + Math.ulp(1e15) + " " + Math.ulp(1e16));
    IO.println(9007199254740993.0);
}
output
2.220446049250313E-16 1.1641532182693481E-10 0.125 2.0
9.007199254740992E15

that last line: 9,007,199,254,740,993 typed as a double comes back as ...992. there is no double for it. this is why IDs and timestamps belong in a long, never a double (and why JSON numbers in javascript, which are all doubles, eat big IDs).

comparing doubles

never == on computed doubles. ask whether they're close enough instead:

Main.java
void main() {
    double sum = 0;
    for (int i = 0; i < 10; i++) sum += 0.1;
    IO.println(sum + " " + (sum == 1.0));
    IO.println(Math.abs(sum - 1.0) < 1e-9);
}
output
0.9999999999999999 false
true

"close enough" depends on your numbers. a fixed tolerance like 1e-9 works around 1, but for values in the millions it's smaller than one ulp, so it never matches. then scale it: Math.abs(a - b) <= 1e-9 * Math.max(Math.abs(a), Math.abs(b)).

predict the output
java
double total = 0;
for (int i = 0; i < 10; i++) total += 0.1;
IO.println(total);

NaN, infinity and negative zero

doubles have a few values that don't exist in math class, and they behave in ways that break normal intuition.

value =
double NaNNaN0x7ff8000000000000
sign · 1 bitexponent · 11 bitsmantissa · 52 bits
0.0 / 0.0→ NaN
nan == nan→ false
Double.isNaN(nan)→ true
Double.valueOf(nan).equals(nan)→ true

not a number: every exponent bit set, a non-zero mantissa. the only value not equal to itself, so == can never find it. use Double.isNaN.

pick a value to see its bits and how it behaves. the amber results are the ones that surprise people. every output here was printed by java 25.

the big one: NaN != NaN. so x == Double.NaN is always false, even when x is NaN. use Double.isNaN(x).

money: BigDecimal

you can't get 0.1 dollars exactly in a double, and money has to add up to the cent. the standard fix is java.math.BigDecimal: an arbitrary-precision decimal, stored as an integer plus a scale (how many digits after the point), so 0.1 really is 1 × 10⁻¹.

don'tmoney in a double
java
double balance = 0;
for (int i = 0; i < 10; i++) balance += 0.10;
IO.println(balance); // 0.9999999999999999
domoney in a BigDecimal
java
var balance = BigDecimal.ZERO;
var dime = new BigDecimal("0.10");
for (int i = 0; i < 10; i++) balance = balance.add(dime);
IO.println(balance); // 1.00

it's slower and wordier (no +, everything is a method call, and every operation returns a new object), but it's exact. the other common choice is to store whole cents in a long and only format at the edges.

one level deeper:what happened to strictfp+

on old x87 CPUs, intermediate results could be kept in 80-bit registers, so the same java code could give slightly different answers on different machines. strictfp forced exact IEEE 754 behaviour. modern CPUs (SSE2 and later) don't have the problem, so java 17 (JEP 306) made every floating-point operation strict, everywhere. the keyword still compiles, it just does nothing, and javac warns: "as of release 17, all floating-point expressions are evaluated strictly and 'strictfp' is not required". same code, same bits, on every JVM.

keylines
  • a double holds the nearest binary fraction to your number. 0.1 isn't one.
  • println shows the shortest digits that identify the double. new BigDecimal(x) shows the truth.
  • never == on computed doubles. compare with a tolerance that fits your numbers.
  • the gaps between doubles grow with size. above 2⁵³ they can't count integers.
  • NaN isn't equal to itself. -0.0 == 0.0. a double divided by zero is Infinity.
  • money: BigDecimal from strings, or a long of cents.