double, BigDecimal, or Fixed-Point?
There is an evergreen debate in the Java world: should you always use BigDecimal for money?
The short answer is no . The real answer is: it depends on your computational context: the precision you need, the rounding rules you must follow, and the performance budget you have.
The problem is that this conversation is often driven by dogma rather than engineering. You hear statements like "`double` is broken," "`BigDecimal` is slow," "always use fixed-point," or "always use IEEE 754," each treated as an absolute truth. Reality is more nuanced and more interesting.
We start from what IEEE 754 actually does under the hood, move through BigDecimal pitfalls and fixed-point arithmetic, tour the libraries that solve these problems for you, and end with the production traps (serialization, testing, concurrency) that can quietly undo a good numeric choice. Sections include working code.
The floating-point problem
Most floating-point surprises trace back to a single fact: Java’s float and double use binary arithmetic, not decimal. Understanding why that matters, and when it doesn’t, is the foundation for every choice in this post.
IEEE 754
float and double in Java follow the IEEE 754 standard: float uses binary32, double uses binary64. The keyword here is binary. Values are stored as a signed bit, an exponent, and a mantissa, all in base 2. This means that many numbers that look trivially simple in base 10 have no exact representation in base 2.
Consider the classic example:
public class FloatingPointProblem {<br>public static void main(String[] args) {<br>double x = 0.1 + 0.2; (1)<br>System.out.println(x); // 0.30000000000000004<br>System.out.println(x == 0.3); // false (2)
0.1 cannot be represented exactly in binary; the compiler stores the nearest binary64 value
Equality check fails because of accumulated representation error
This does not mean that double is broken. It means that the decimal literal 0.1 cannot be represented exactly in binary, just as 1/3 cannot be represented exactly in decimal. When you write 0.1 in Java, the compiler stores the nearest binary64 value: 0.1000000000000000055511151231257827021181583404541015625. Every subsequent operation compounds that tiny initial gap.
double is fast, compact, and purpose-built for numerical computation, but it is inherently approximate in the decimal sense. That is not a defect; it is a design trade-off, and in many domains it is exactly the right one.
Naive equality checks
The most common floating-point bug is not imprecision itself but testing equality with ==.
Never do this:
if (result == expected) { ... } // dangerous with floating-point
Instead, compare within a tolerance (often called epsilon):
boolean nearlyEqual(double a, double b, double epsilon) {<br>return Math.abs(a - b) epsilon;
This works well when your values live in a known range. But if the magnitudes vary widely (say from 0.0001 to 1_000_000), a fixed epsilon is either too tight for large values or too loose for small ones. In that case, use a relative tolerance:
boolean nearlyEqualRelative(double a, double b, double relTol) {<br>double diff = Math.abs(a - b);<br>double norm = Math.max(Math.abs(a), Math.abs(b));<br>return diff relTol * norm;
The choice between absolute and relative tolerance depends on your data. Scientific applications often use relative tolerance; financial rounding at display boundaries often uses a fixed number of decimal places. The point is: understand your comparison strategy before you write a single if.
strictfp, the x87 FPU, and Java 17’s silent fix
Java 1.0 enforced strict IEEE 754 semantics, but Intel’s x87 FPU computed intermediates in 80-bit extended precision, making strict mode slow. Java 1.2 introduced a compromise:
default mode, which allowed extended exponent range, producing subtly different results across hardware)<br>strict mode, exact IEEE 754, enabled with strictfp
Almost nobody used strictfp, so floating-point results could differ across platforms, exactly the kind of non-determinism that makes numerical debugging a nightmare.
JEP 306 in Java 17 resolved this. Modern hardware (SSE2, AVX) supports strict IEEE 754 natively with no penalty, so JEP 306 restored always-strict semantics as the default. The strictfp modifier is now a no-op, accepted for backward compatibility, but with no effect.
The practical implication: on Java 17+, floating-point arithmetic is fully reproducible across platforms. If you still see strictfp in legacy code, it can safely be removed.
The NaN and -0.0 landmines
IEEE 754 defines two special values that break common assumptions about how numbers behave.
NaN (Not a Number)* has a unique property: it is not equal to itself. This is mandated by the standard, and Java follows it faithfully:
double nan = Double.NaN;<br>System.out.println(nan == nan); // false (1)<br>System.out.println(Double.compare(nan, nan)); // 0 (2)<br>System.out.println(Double.isNaN(nan)); // true
The == operator says NaN !=...