The full picture
0.1 + 0.2 == 0.3 // false
0.1 + 0.2 // 0.30000000000000004
double big = 9_007_199_254_740_992.0; // 2^53
big + 1 == big // true — spacing is now 2.0
Double.NaN == Double.NaN // false (IEEE rule)
Double.valueOf(Double.NaN).equals(Double.NaN) // true! (Java containers need it)
double x = -0.0; x == 0.0 // true, but 1/x = -Infinity
// Money: never double.
BigDecimal price = new BigDecimal("19.99"); // STRING ctor — new BigDecimal(19.99)
// captures the double's error!
long cents = 1999L; // or integers in minor units- The mental model that generates all the rules: floats are fixed relative precision — ~16 digits wherever you are on the number line; absolute error grows with magnitude. Epsilon comparisons must therefore be relative (or ULP-based), not a fixed 1e-9.
- Summation order matters: adding tiny numbers to a big accumulator loses them one at a time (catastrophic when averaging millions of values) — Kahan summation or pairwise/streaming approaches;
DoubleStream.sum()already does better-than-naive summation. - Catastrophic cancellation: subtracting nearly-equal numbers annihilates significant digits — the quadratic-formula rewrite is the textbook case; variance computed as E[X²]−E[X]² going negative is the one you'll actually meet in metrics code.
- Bonus currency: float16/bfloat16 exist because ML traded precision for throughput — knowing bfloat16 keeps float32's exponent range with 8 mantissa bits shows the trade-off thinking transfers to 2026 problems.