JAVA / VARIABLES, PRIMITIVES AND TYPES
Floating point precision and the float versus double choice
Predict which decimal values a double cannot store exactly, compare floating point results safely, and choose between float, double and BigDecimal.
What you will learn
- Explain why 0.1 + 0.2 == 0.3 is false and print the stored value with BigDecimal
- Compare doubles with a tolerance sized to the magnitudes involved, never with ==
- Pick double by default and float only when 4-byte storage is the real constraint
- Hold money as a long count of minor units or a BigDecimal built from a String
Understanding Floating point precision and the float versus double choice
A double stores a number as significand x 2^exponent with 53 bits of significand, so the only values it holds exactly are sums of powers of two. One tenth is not one of them: 1/10 in binary is 0.0001100110011... repeating forever, exactly the way 1/3 repeats in decimal. The compiler therefore stores the nearest double to 0.1, which is 0.1000000000000000055511151231257827021181583404541015625, and every later calculation starts from that value instead of from 0.1.
Each arithmetic operation takes the exact result of its two stored operands and rounds that to the nearest representable value. Errors sometimes cancel, so 0.1 + 0.1 is bit-for-bit the double named 0.2, and sometimes accumulate, which is why adding 0.1 ten times finishes at 0.9999999999999999 rather than 1.0. Printing hides all of it, because println emits the shortest decimal that reads back as the same double, so 0.1 shows as 0.1 and the gap only surfaces when a comparison or a further operation exposes it.
float is the same scheme with 24 significand bits packed into 4 bytes: roughly 7 useful decimal digits instead of 16, and exact integers only up to 2^24 = 16777216 instead of 2^53. Its gaps are 2^29 times wider than double's, so accumulated error in a long float sum grows correspondingly faster. Java pushes you toward double anyway: 3.14 is a double, a float literal needs the f suffix, and Math methods take and return doubles, so float code collects casts. Reach for float when you hold millions of values and memory or bandwidth is the binding constraint, or when a file format or API requires it.
import java.math.BigDecimal;
public class Precision {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
System.out.println(a + b);
System.out.println(a + b == 0.3);
System.out.println(new BigDecimal(a));
System.out.println(new BigDecimal(0.1f));
double sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += 0.1;
}
System.out.println(sum);
System.out.println(Math.abs(sum - 1.0) < 1e-9);
}
}A float or double holds the nearest value of the form significand x 2^exponent, so any decimal that is not a sum of powers of two is stored slightly wrong and stays slightly wrong through every operation.
Worked examples
Where float runs out of digits
Shows the 24-bit significand of float losing an integer that double keeps, and the difference in decimal digits.
public class FloatDigits {
public static void main(String[] args) {
float f = 16777217f;
System.out.println(f);
System.out.println(f == 16777216f);
System.out.println(16777217.0 == 16777216.0);
System.out.println(1.0f / 3.0f);
System.out.println(1.0 / 3.0);
}
}Example explained
Line 1Above 2^24 the gap between neighbouring floats is 2, so the literal 16777217f is rounded down to 16777216 at compile time with no error or warning.
Line 2The float comparison is true because both literals became the same float; the double comparison is false because double keeps every integer up to 2^53.
Line 31.0f / 3.0f needs 8 decimal digits to identify it uniquely, 1.0 / 3.0 needs 16: that is 24 versus 53 significand bits showing up in decimal.
Line 4Neither division is exact, since 1/3 has no finite binary form at any precision.
Comparing without ==
Demonstrates that a fixed tolerance is a statement about scale, and that at large magnitudes even adding 1 disappears.
public class CompareDoubles {
public static void main(String[] args) {
double a = 0.1 + 0.2;
System.out.println(a == 0.3);
System.out.println(Math.abs(a - 0.3) < 1e-9);
double tiny = 1e-12;
System.out.println(tiny == 0.0);
System.out.println(Math.abs(tiny - 0.0) < 1e-9);
double big = 1e16;
System.out.println(big + 1 == big);
}
}Example explained
Line 1a sits one step above the double nearest 0.3, so == reports a difference even though the two agree to sixteen digits.
Line 2A 1e-9 tolerance is well chosen there, because the actual difference is about 5.6e-17.
Line 3The same tolerance is useless near zero: 1e-12 is a genuinely non-zero value a thousand times smaller than the tolerance, so the test calls it equal to 0.
Line 4At 1e16 the spacing between neighbouring doubles is 2, so big + 1 rounds back to big and any absolute tolerance under 2 is meaningless.
Money: cents or BigDecimal
Shows a price scaled by 100 landing just below a whole number, and the two representations that avoid the problem.
import java.math.BigDecimal;
public class Money {
public static void main(String[] args) {
double price = 4.35;
System.out.println(price * 100);
System.out.println((int) (price * 100));
System.out.println(Math.round(price * 100));
long cents = 435;
System.out.println(cents * 3);
System.out.println(new BigDecimal("4.35").multiply(new BigDecimal("3")));
}
}Example explained
Line 1The nearest double to 4.35 is a hair below 4.35, and multiplying by 100 keeps that shortfall, so the product lands at 434.99999999999994.
Line 2(int) truncates toward zero, turning a 435-cent price into 434: one cent lost per line, silently.
Line 3Math.round goes to the nearest long and recovers 435, but only because the error is far smaller than 0.5.
Line 4long cents and BigDecimal("4.35") both stay exact, and BigDecimal keeps the scale so the total prints as 13.05 rather than 13.050000000000001.
Important notes
println shows the shortest decimal that maps back to the same value, not the stored value, so 0.1 and 0.1f both print as 0.1 even though they are different numbers.
float is not a workaround: 0.1f + 0.2f == 0.3f is true only because float's coarser rounding happens to land on the nearest float to 0.3, which is luck rather than accuracy.
Common mistakes
Driving a loop with a floating point equality, as in for (double x = 0; x != 1.0; x += 0.1): the counter goes 0.9999999999999999 then 1.0999999999999999, never hits 1.0, and the loop never ends.
Casting when you meant to round: (int) (4.35 * 100) gives 434 because the product is 434.99999999999994 and the cast truncates, so every scaled price loses a cent.
Writing new BigDecimal(0.1) instead of new BigDecimal("0.1"): the double constructor imports the binary error, producing a 55-decimal-place value whose equals and totals no longer match what you typed.
Try it yourself
Change, predict, then run
Add 0.01 to a double one hundred times in a loop, then print the sum, the result of sum == 1.0, and new BigDecimal(sum) to see exactly where it landed.
Open the Java workspaceCheck your understanding
0.5 + 0.25 == 0.75 evaluates to true, while 0.1 + 0.2 == 0.3 evaluates to false. What explains the difference?
- The first expression needs fewer decimal digits, so less precision is required to hold it.
- The compiler folds simple fractions symbolically and only evaluates awkward ones at runtime.
- 0.5, 0.25 and 0.75 are exact multiples of powers of two, so nothing is rounded, while 0.1, 0.2 and 0.3 are each stored as approximations.
- The second expression exceeds the significand size of double, so it overflows into a rounding error.
Show answer
Exactness depends on the denominator being a power of two: 1/2, 1/4 and 3/4 fit binary fractions exactly, so both operands and the sum are stored with no error. Digit count, the tempting answer, is irrelevant: 0.078125 is 5/64 and is exact with six decimal digits, while 0.1 has one digit and cannot be represented at all.