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.
 

Comments:

ubm_techweb_disqus_sso_-06a6faddc430162ab6c827d900667643
2014-08-22T19:04:26

Thank you for that thorough explanation. I'll check out that paper. Thanks again.


Permalink
ubm_techweb_disqus_sso_-dde5b411bc005defe8b5727c3b192ce7
2014-08-21T23:55:33

Floating point does give exact answers given the right conditions.

E.g. if you supply a value with five decimal digits before conversion to decimal, you should convert to a maximum of five digits when converting back to decimal.

Double gives you 16 decimal digits of precision. So if you convert a decimal number with 16 digit or less to FP and back you get the exact result. But you need to round to the number of significant digits you expect since FP does not keep track of that for you.

Then you may lose precision during operations, For multiplication and division, a rule of thumb is that you lose at most one digit per operation. For addition and subtraction it is more complicated, but think of it as aligning around the decimal point. If the maximum number of digit before and after the decimal point, including the result, overall is larger than 16 digit, you lose precision, otherwise you don't.

In your example you need eight digits, well withing the precision of double, so the result when converted back to decimal with the significant number of digits, is in fact exact. The error in the original code is that it outputs the results with excess precision.

IEEE floating point is exact, as long as you don't overstep the precision, which means you need to estimate the total error in your calculations regardless of whether you are counting pennies or forecasting the weather.

For money this is problematic because most calculations fit fine within the precision of double, but just barely and many accountants expect that even amounts of a thousand trillions are computed to the penny, because that's how debit and credit are balanced.

As for "can't use for money", I think that statement is invalid. Excel is the most widely used tool for monetary calculations and it uses floating point. Sure people make huge mistakes in their spreadsheets, but that's not due to the use of floating point. Trading systems also use floating point heavily, so apparently it works quite well.

C/C++ programmers on many platforms can use long double which has 35 or so decimal digits which allows for quite a lot of calculations before any significant loss of precision occurs.

Of course the safe way is to use BigDecimal and only BigDecimal. You mix double and BigDecimal which bad and is likely to get you into trouble someday. I've seen worse though. BigDecimal has a constructor from String, so you could write this instead that has no floating point at all.

BigDecimal a = new BigDecimal("106838.81");
BigDecimal b = new BigDecimal("263970.96");
BigDecimal c = new BigDecimal("879.35");
BigDecimal d = new BigDecimal("366790.80");

BigDecimal total = BigDecimal.ZERO;
total = total.add( a );
total = total.add( b);
total = total.subtract( c );
total = total.subtract( d );

There is a paper called "What Every Computer Scientist Should Know About Floating Point" that explains all the details of when you can expect exact results from floating point.


Permalink
ubm_techweb_disqus_sso_-f5556c08afb65764ed6cd9f93eab3cf0
2014-08-04T14:57:39

I think it's not just that people don't know about BigDecimal (they probably pass it in JavaDoc many times), but that using BigDecimal means (1) your formulas become far more obtuse and hard to read, and (2) you can't use floating-point libraries and frameworks.

Leaving operator overloading out of Java was probably a good thing, but it makes it a bad language for finance. I'd say just use Scala, but then you'd have to spend two years learning Scala-- which is made harder because of... operator overloading.


Permalink
dblake950
2014-07-30T17:49:59

Ha! You caught us tripping over our attempts to be clever in headline writing. In all fairness to Eric, that was my goof in editing the blog post. The correction has been made.


Permalink
ubm_techweb_disqus_sso_-de3d4e3abc86260f1fcf0b21fa0a1056
2014-07-29T21:00:57

"Inprecise" is more imprecise than floating-point! :)


Permalink
ubm_techweb_disqus_sso_-c061fb372c994ad30955829504bffd57
2014-07-29T20:53:41

I am more mystified by the title. I was looking for the 'hook' as to why floating point math is 'in-precise' as opposed to 'imprecise'. I guess it was just a typo. Please be more careful when using terms that are either incorrect or imprecise, as they are more misleading in the context in which they are used.


Permalink
AndrewBinstock
2014-07-24T22:54:51

It is ridiculous, as you state. This problem has been known for years and, esp. in Java, there are classes specifically created to solve this problem (that is, the BigDecimal that Eric discusses). This is where, I believe, the self-taught history of some programmers comes back to bite them: they don't know what they don't know.


Permalink
ubm_techweb_disqus_sso_-24678308430b00333ea27b7db31b851a
2014-07-24T22:09:08

I'm utterly depressed by the fact that twenty years after even I started working in banking I.T there still seem to be people who don't know that you can't use fp data types for financial calculations.
Please RTFM!
No matter what the language I'm sure it will say the same thing.


Permalink
ubm_techweb_disqus_sso_-06a6faddc430162ab6c827d900667643
2014-07-24T18:10:32

Thanks for the comment. I focused on Java since this is a Java specific blog, that's why I didn't say it was a flaw or bug. I didn't compare it to the behavior of other compilers and runtimes. Instead I focused on how to properly handle floating point math for financial calculations in Java using BigDecimal. I apologize if the title is misleading.


Permalink
ubm_techweb_disqus_sso_-701a860b67c209814860005dc2a7e9c5
2014-07-24T15:03:32

I'm mystified by this article. Not the observed behavior, but the explanation.

Here's the output from the same initial code using C++ (g++ on Linux Mint):

total: 3139.6200000000535510

That's the same result because it's calculated using IEEE 754 double precision floating point math - just as it is in Java. IEEE floating point math is well known to be unsuitable for financial calculations.

Please make it clear that this is in no way unique to Java, as even the title implies. It's simply a function of the normal behavior of IEEE floating point.


Permalink


Video