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
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.
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:
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));
}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.
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:
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.void main() {
IO.println(Math.ulp(1.0) + " " + Math.ulp(1e6) + " " + Math.ulp(1e15) + " " + Math.ulp(1e16));
IO.println(9007199254740993.0);
}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:
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);
}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)).
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.
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.
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⁻¹.
double balance = 0;
for (int i = 0; i < 10; i++) balance += 0.10;
IO.println(balance); // 0.9999999999999999var balance = BigDecimal.ZERO;
var dime = new BigDecimal("0.10");
for (int i = 0; i < 10; i++) balance = balance.add(dime);
IO.println(balance); // 1.00it'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.
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.
- a double holds the nearest binary fraction to your number. 0.1 isn't one.
printlnshows 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:
BigDecimalfrom strings, or alongof cents.