Java's Floating-Point (Im)Precision
I was working with someone recently who told me he was having intermittent issues dealing with double floating-point values in Java. When he added and subtracted dollar amounts in this financial application, using the Java primitive type double
, he would sometimes get unexpected results. For example, the following code shows a sample of some calculations he found troubling (other combinations of values worked fine):
double a = 106838.81; double b = 263970.96; double c = 879.35; double d = 366790.80; double total = 0; total += a; total += b; total -= c; total -= d;
At the end of this operation, he expected the value 3139.62
, but instead was getting 3139.6200000000536
. When formatted for display as a dollar amount, it was appearing as expected: "$3139.62." However, when he later inspected the database where he stored results, he noticed the issue. The trouble only came to light when he started seeing "$-0.00" after formatting for currency display. A quick look at the database showed the actual value as -0.000000000053518078857450746
. What was going on to cause this, and how could it be fixed?
The problem derives from how float
and double
floating-point values are stored internally by the JVM. Unlike int
and long
(and other fixed-point types) that are stored as exact binary representations of the numbers they're assigned to, shortcuts are taken with float
and double
. Internally, Java stores values for these types with an inexact representation, using only a portion of the 64 bits for the significant digits. As a result, Java doesn't store, calculate, or return the exact representation of the actual floating-point value in some cases. This seemingly intermittent behavior can be annoying, as it only becomes apparent with specific combinations of numbers and operations.
BigDecimal to the Rescue
Fortunately, Java provides a math package, java.math.*, which includes the BigDecimal
class. BigDecimal
can be used to alleviate the rounding and loss of precision issues that are often seen with double floating-point arithmetic. BigDecimal
allows you to specify precisely how the rounding behavior should work using the java.math.MathContext
class. The number of digits to be returned can be specified with this object as well. Let's look at some examples:
// The following code returns 1.5500000000000000444089209850062616169452667236328125 BigDecimal bd = new BigDecimal(1.55); // The following code returns 1.550000 BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL32); // The following code returns 1.550000000000000 BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL64);
Above, you see how the constructor allows you to specify the precision used to store and work with the floating-point value. Let's look at how to specify rounding, which must occur when the exact value cannot be represented with the precision used. First, note that the scale of the BigDecimal
floating-point value indicates the number of digits to the right of the right of the decimal point.
Each of the following returns 1.55:
BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL32); bd = bd.setScale(2); BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL64); bd = bd.setScale(2);
However, due to the default precision used, the following throws an exception indicating that rounding is necessary:
BigDecimal bd = new BigDecimal(1.55); bd = bd.setScale(2);
There are multiple rounding types (to round up, down, using ceiling or floor operators, and so on), and you can specify the kind you want as the second parameter when setting the scale:
BigDecimal bd = new BigDecimal(1.55); bd = bd.setScale(2, BigDecimal.ROUND_DOWN);
Remember that the BigDecimal
class is immutable, and simply calling a method on a BigDecimal
object will have no affect, so you need to reassign it after every call. Let's revisit the first example showing the set of floating-point operations that yielded unexpected results, but modified to use BigDecimal
properly:
double a = 106838.81; double b = 263970.96; double c = 879.35; double d = 366790.80; BigDecimal total = new BigDecimal(0, MathContext.DECIMAL64); total = total.setScale(2); total = total.add(new BigDecimal(a, MathContext.DECIMAL64)); total = total.add(new BigDecimal(b, MathContext.DECIMAL64)); total = total.subtract(new BigDecimal(c, MathContext.DECIMAL64)); total = total.subtract(new BigDecimal(d, MathContext.DECIMAL64));
In this case, the precision is set to 64 bits, and the scale is set to 2 to adequately represent currency values. Additionally, the results of calls to add and subtract are reassigned to the original BigDecimal
object because it's immutable. I avoid the verbose code (and the typing required) to set the precision and scale, and instead use helper methods, such as:
private BigDecimal doubleToBD32(double val) { return new BigDecimal(val, MathContext.DECIMAL64).setScale(2); } private BigDecimal doubleToBD64(double val) { return new BigDecimal(val, MathContext.DECIMAL64).setScale(2); } private BigDecimal doubleToBD128(double val) { return new BigDecimal(val, MathContext.DECIMAL128).setScale(2); } private BigDecimal doubleToBD(double val) { return new BigDecimal(val, MathContext.UNLIMITED).setScale(2); }
As a result, the final code looks like this, which is more pleasing to me:
double a = 106838.81; double b = 263970.96; double c = 879.35; double d = 366790.80; BigDecimal total = doubleToBD64(0); total = total.add( doubleToBD64(a) ); total = total.add( doubleToBD64(b) ); total = total.subtract( doubleToBD64(c) ); total = total.subtract( doubleToBD64(d) );
Although there are many, many more details involved and other options when using BigDecimal
and MathContext
(and the java.math package for that matter), I hope this quick overview helps if you ever get bit by Java's binary representation of floating-point and double floating-point numbers and arithmetic operations.
Happy coding!
EJB