Channels ▼

Eric Bruno

Dr. Dobb's Bloggers

Java's Floating-Point (Im)Precision

July 24, 2014

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

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 


Video