Understanding BigDecimal Precision Issues in Monetary Calculations and a Utility Class for Accurate Operations
This article explains why using floating‑point types with BigDecimal can cause precision loss in financial calculations, demonstrates the problem with sample code, analyzes the underlying double‑to‑long conversion, and provides a reusable Java utility class to perform accurate arithmetic with BigDecimal.
When handling monetary values, developers often use BigDecimal because it avoids the precision problems that arise with float or double . Incorrect use of its constructors, however, can still lead to loss of precision and even financial loss.
Background
In a cash‑register system an incident occurred where the order could not be paid due to a calculation error caused by BigDecimal misuse, dropping the payment success rate to 60%.
Problem Description
Cash register failed to compute product amounts, resulting in order payment failure.
Incident Level
P0 (critical).
Process
13:44 – Alert received, payment failure.
13:50 – Quick rollback of the deployed code, service restored.
14:20 – Code review and pre‑release verification uncovered the issue.
14:58 – Fixed code deployed, system fully recovered.
Root Cause
The loss of precision stemmed from using double (or float ) values when constructing BigDecimal . The underlying doubleToRawLongBits conversion from double to long (implemented in native C++) loses exact decimal representation because binary floating‑point cannot represent many decimal fractions precisely.
Analysis
Reproducing the issue:
public static void main(String[] args) {
BigDecimal bigDecimal = new BigDecimal(88);
System.out.println(bigDecimal);
bigDecimal = new BigDecimal("8.8");
System.out.println(bigDecimal);
bigDecimal = new BigDecimal(8.8);
System.out.println(bigDecimal);
}Running the code shows that the constructor receiving a double produces a value with unexpected trailing digits, while the String and int constructors retain the exact value.
Inspecting the source of Double.doubleToLongBits reveals that it ultimately relies on binary representation, which cannot exactly encode many decimal fractions, leading to the observed precision loss.
Conclusion
For any calculation that requires exact decimal precision, especially monetary amounts, always construct BigDecimal from a String (or use BigDecimal.valueOf(double) which internally uses Double.toString ) and avoid direct double / float constructors.
Correct usage example:
BigDecimal bd1 = new BigDecimal("8.8");
BigDecimal bd2 = new BigDecimal("8.812");
System.out.println(bd1.compareTo(bd2));
System.out.println(bd1.add(bd2));Because BigDecimal objects are immutable, arithmetic must be performed via its methods rather than the traditional operators.
Tool Sharing
A utility class is provided to simplify accurate arithmetic operations with BigDecimal for both double and float inputs.
import java.math.BigDecimal;
/**
* @Author shuaige
* @Date 2022/4/17
* @Version 1.0
*/
public class BigDecimalUtils {
public static BigDecimal doubleAdd(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2);
}
public static BigDecimal floatAdd(float v1, float v2) {
BigDecimal b1 = new BigDecimal(Float.toString(v1));
BigDecimal b2 = new BigDecimal(Float.toString(v2));
return b1.add(b2);
}
public static BigDecimal doubleSub(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2);
}
public static BigDecimal floatSub(float v1, float v2) {
BigDecimal b1 = new BigDecimal(Float.toString(v1));
BigDecimal b2 = new BigDecimal(Float.toString(v2));
return b1.subtract(b2);
}
public static BigDecimal doubleMul(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2);
}
public static BigDecimal floatMul(float v1, float v2) {
BigDecimal b1 = new BigDecimal(Float.toString(v1));
BigDecimal b2 = new BigDecimal(Float.toString(v2));
return b1.multiply(b2);
}
public static BigDecimal doubleDiv(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
// Keep two decimal places, round half up
return b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP);
}
public static BigDecimal floatDiv(float v1, float v2) {
BigDecimal b1 = new BigDecimal(Float.toString(v1));
BigDecimal b2 = new BigDecimal(Float.toString(v2));
return b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP);
}
/**
* Compare two double values using BigDecimal
*/
public static int doubleCompareTo(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.compareTo(b2);
}
public static int floatCompareTo(float v1, float v2) {
BigDecimal b1 = new BigDecimal(Float.toString(v1));
BigDecimal b2 = new BigDecimal(Float.toString(v2));
return b1.compareTo(b2);
}
}Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.