Double or BigDecimal for Monetary Fields? Precision and Performance Trade‑offs
The article dissects the long‑standing Java debate over using double versus BigDecimal for money, explaining that the choice hinges on required precision, rounding rules, and performance limits, and walks through floating‑point fundamentals, common pitfalls, and high‑accuracy summation techniques.
Introduction
Amount calculations in Java require a choice between double and BigDecimal. The decision depends on required decimal precision, legal rounding rules, and the performance budget of the system.
Floating‑point pitfalls
Java float and double implement IEEE 754 binary32 and binary64 formats. Decimal literals such as 0.1 cannot be represented exactly in binary, leading to small representation errors that accumulate.
public class FloatingPointProblem {
public static void main(String[] args) {
double x = 0.1 + 0.2;
System.out.println(x); // 0.30000000000000004
System.out.println(x == 0.3); // false
}
}Using == for equality is unsafe. Compare within a tolerance:
boolean nearlyEqual(double a, double b, double epsilon) {
return Math.abs(a - b) <= epsilon;
}
boolean nearlyEqualRelative(double a, double b, double relTol) {
double diff = Math.abs(a - b);
double norm = Math.max(Math.abs(a), Math.abs(b));
return diff <= relTol * norm;
}NaN and –0.0 traps
IEEE 754 defines special values: NaN is never equal to itself; Double.isNaN() must be used before logical checks.
Double.compare()</code treats two NaNs as equal for ordering.</li><li><code>-0.0compares equal to 0.0 with == but Double.compare() distinguishes them, and division by -0.0 yields -Infinity.
public class Double_NaN_Zero {
public static void main(String[] args) {
double nan = Double.NaN;
System.out.println(nan == nan); // false
System.out.println(Double.compare(nan, nan)); // 0
System.out.println(Double.isNaN(nan)); // true
System.out.println(0.0 == -0.0); // true
System.out.println(Double.compare(0.0, -0.0)); // 1
System.out.println(1.0 / 0.0); // Infinity
System.out.println(1.0 / -0.0); // -Infinity
}
}When double is the right choice
Use double when:
Small rounding errors are acceptable (e.g., ranking scores, percentages).
The data source is inherently noisy (sensor data, GPS).
Performance is critical; hardware FPU executes double arithmetic orders of magnitude faster than arbitrary‑precision arithmetic.
Rounding is deferred to the final presentation or persistence step.
Naïve averaging illustrates error accumulation:
static double average(double[] values) {
double sum = 0.0;
for (double v : values) sum += v;
return sum / values.length;
}With millions of elements, each addition introduces a few ULPs, causing noticeable drift.
Reducing floating‑point error
Kahan compensated summation tracks the lost low‑order bits and feeds them back into the next addition.
public class Double_Kahan {
static double kahanSum(double[] values) {
double sum = 0.0;
double compensation = 0.0;
for (double value : values) {
double y = value - compensation;
double t = sum + y;
compensation = (t - sum) - y;
sum = t;
}
return sum;
}
}Benchmark (10 000 000 elements) shows ordinary summation error ≈ 0.192, while Kahan reduces it to ≈ 1.2 × 10⁻⁷. The Neumaier variant fixes the case where a term’s magnitude exceeds the running sum.
static double neumaierSum(double[] values) {
double sum = 0.0;
double compensation = 0.0;
for (double value : values) {
double t = sum + value;
if (Math.abs(sum) >= Math.abs(value)) {
compensation += (sum - t) + value;
} else {
compensation += (value - t) + sum;
}
sum = t;
}
return sum + compensation;
}Example output:
常规求和: 1000000001.1920928
Kahan求和: 1000000000.9999999
理论精确值: 1000000001
常规误差: 0.192092776299
Kahan误差: 0.000000119209Fused Multiply‑Add (FMA)
Since Java 9, Math.fma(a, b, c) performs a * b + c with a single rounding step, matching the IEEE 754‑2008 fusedMultiplyAdd operation. On CPUs that support FMA, this is often faster than the two‑instruction sequence.
double standard = a * b + c; // two roundings
double fma = Math.fma(a, b, c); // one roundingJDK and third‑party support
DoubleStream.sum()Javadoc notes that implementations may use compensated summation or other techniques to reduce error.
Apache Commons Numbers ( commons-numbers-core) provides Sum2S and Dot2S (Ogita‑Rump‑Oishi 2005) algorithms and a Precision utility for epsilon/ULP comparisons.
What BigDecimal actually solves
BigDecimalstores an unscaled integer and a scale, giving exact decimal representation (e.g., 19.99). It offers arbitrary precision, explicit rounding control, and is required for legally mandated rounding such as tax calculations.
Common pitfalls:
Constructing from a double captures the binary approximation. Use new BigDecimal("0.1") or BigDecimal.valueOf(0.1) instead.
Immutability: methods return new instances; forgetting to assign the result leaves the original unchanged. equals() compares both value and scale. Use compareTo() == 0 for numeric equality and stripTrailingZeros() when using as map keys.
Performance cost
BigDecimalallocates heap objects, performs arbitrary‑precision integer arithmetic, and manages scale metadata, making it roughly 100 × slower than double in tight loops.
@Benchmark
public void doubleCalc(Blackhole bh) {
double result = (100.10 + 200.20) / 2.0;
bh.consume(result);
}
@Benchmark
public void bigDecimalCalc(Blackhole bh) {
BigDecimal a = new BigDecimal("100.10");
BigDecimal b = new BigDecimal("200.20");
BigDecimal result = a.add(b).divide(BigDecimal.valueOf(2), 2, RoundingMode.HALF_UP);
bh.consume(result);
}Rounding a double to two decimal places with a cast‑based method costs ~6 ns, whereas BigDecimal.setScale costs ~932 ns.
static double roundToTwoPlaces(double d) {
return ((long) (d < 0 ? d * 100 - 0.5 : d * 100 + 0.5)) / 100.0;
}Comparative summary
Naïve summation – error O(n), cost 1×, parallelizable, suitable for low‑precision tasks.
Pairwise (divide‑and‑conquer) summation – error ~1×, cost ~1×, parallelizable, general‑purpose (used in NumPy, Julia).
Kahan compensation – error O(1), cost ~4×, not parallelizable (data dependency), best for high‑precision sequential calculations.
Neumaier improvement – error O(1), cost ~4×, not parallelizable, handles mixed‑magnitude data.
Apache Commons Numbers Sum – error ~4×, cost ~4×, not parallelizable, production‑grade Java library.
Conclusion
There is no universal answer. Choose double when speed matters and modest rounding error is acceptable; switch to BigDecimal for exact decimal semantics, legal rounding, or when the performance budget permits. For large aggregations, employ compensated algorithms such as Kahan or library‑provided summation utilities to balance accuracy and speed.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
