7

I tried the following code. but getting different result when subtracting using BigDecimal.

    double d1 = 0.1;
    double d2 = 0.1;
    System.out.println("double result: "+ (d2-d1));

    float f1 = 0.1F;
    float f2 = 0.1F;
    System.out.println("float result: "+ (f2-f1));

    BigDecimal b1 = new BigDecimal(0.01);
    BigDecimal b2 = new BigDecimal(0.01);

    b1 = b1.subtract(b2);
    System.out.println("BigDecimal result: "+ b1);

Result:

double result: 0.0
float result: 0.0
BigDecimal result: 0E-59

I am still working on this. can anyone please clarify.

user1514499
  • 762
  • 7
  • 26
  • 63
  • 0E-59 is still zero, and the scale is derived from the scale of the inputs. – user207421 Mar 16 '13 at 00:24
  • The answers cover the reason and here is a simple tip: if you use a BigDecimal you need some useful scale, hence when you invoke the c-tor w/ some double value, always include MathContext. – bestsss Mar 17 '13 at 11:05

8 Answers8

11

[There are a lot of answers here telling you that binary floating-point can't exactly represent 0.01, and implying that the result you're seeing is somehow inexact. Whilst the first part of that is true, it's not really the core issue here.]


The answer is that "0E-59" is equal to 0. Recall that a BigDecimal is the combination of an unscaled value and a decimal scale factor:

System.out.println(b1.unscaledValue());
System.out.println(b1.scale());

displays:

0
59

The unscaled value is 0, as expected. The "strange" scale value is simply an artifact of the decimal expansion of the non-exact floating-point representation of 0.01:

System.out.println(b2.unscaledValue());
System.out.println(b2.scale());

displays:

1000000000000000020816681711721685132943093776702880859375
59

The next obvious question is, why doesn't BigDecimal.toString just display b1 as "0", for convenience? The answer is that the string representation needs to be unambiguous. From the Javadoc for toString:

There is a one-to-one mapping between the distinguishable BigDecimal values and the result of this conversion. That is, every distinguishable BigDecimal value (unscaled value and scale) has a unique string representation as a result of using toString. If that string representation is converted back to a BigDecimal using the BigDecimal(String) constructor, then the original value will be recovered.

If it just displayed "0", then you wouldn't be able to get back to this exact BigDecimal object.

Oliver Charlesworth
  • 267,707
  • 33
  • 569
  • 680
  • I tried with the BigDecimal b1 = new BigDecimal(0.05); BigDecimal b2 = new BigDecimal(0.01); getting BigDecimal result 1: 4000000000000000256739074444567449972964823246002197265625 BigDecimal result 2: 59. similar to yours... – user1514499 Mar 15 '13 at 09:47
  • @user1514499: Ok, well that's a different question! But the answer is related; you can't exactly represent `0.01` or `0.05` in binary floating-point. They are approximated. – Oliver Charlesworth Mar 15 '13 at 09:49
  • Damn, as I was editing in that last snippet (the birectional conversion stuff) into my answer, I came up for air and found that you'd already nailed it :-) – paxdiablo Mar 15 '13 at 10:13
  • +1 I learned something new. Well explained! (Deleted my answer since it's irrelevant) – Eyal Schneider Mar 15 '13 at 10:20
7

Use constructor from String: b1 = new BigDecimal("0.01");

Java loss of precision

(slide 23) http://strangeloop2010.com/system/talks/presentations/000/014/450/BlochLee-JavaPuzzlers.pdf

Community
  • 1
  • 1
Ivan Borisov
  • 413
  • 2
  • 7
4

Interesting, the values appear to be equal and subtraction does give you zero, it appears to just be an issue with the printing code. The following code:

import java.math.BigDecimal;
public class Test {
    public static void main(String args[]) {
        BigDecimal b1 = new BigDecimal(0.01);
        BigDecimal b2 = new BigDecimal(0.01);
        BigDecimal b3 = new BigDecimal(0);
        if (b1.compareTo(b2) == 0) System.out.println("equal 1");
        b1 = b1.subtract(b2);
        if (b1.compareTo(b3) == 0) System.out.println("equal 2");
        System.out.println("BigDecimal result: "+ b1);
    }                          
}

outputs both equal messages, indicating that the values are the same and that you get zero when you subtract.

You could try to raise this as a bug and see what Oracle comes back with. It's likely they'll just state that 0e-59 is still zero, so not a bug, or that the rather complex behaviour being described on the BigDecimal documentation page is working as intended. Specifically, the point that states:

There is a one-to-one mapping between the distinguishable BigDecimal values and the result of this conversion. That is, every distinguishable BigDecimal value (unscaled value and scale) has a unique string representation as a result of using toString. If that string representation is converted back to a BigDecimal using the BigDecimal(String) constructor, then the original value will be recovered.

That fact that the original value needs to be recoverable means that toString() needs to generate a unique string for each scale, which is why you're getting 0e-59. Otherwise, converting the string back to a BigDecimal may give you a different value (unscaled-value/scale tuple).

If you really want zero to show up as "0" regardless of the scale, you can use something like:

if (b1.compareTo(BigDecimal.ZERO) == 0) b1 = new BigDecimal(0);
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • 1
    I don't think it's a bug; `BigDecimal.toString` prints the canonical representation, so it has to represent both the scale factor and the unscaled value, in an unambiguous manner. The scale factor is 59 because the scale factor of the inputs to the subtraction were 59... – Oliver Charlesworth Mar 15 '13 at 09:52
  • @Oli, actually you're right, this behaviour is intentional per the doco. – paxdiablo Mar 15 '13 at 10:05
2

You have to get the return value:

BigDecimal b3 = b1.subtract(b2);
System.out.println("BigDecimal result: "+ b3);
ghdalum
  • 891
  • 5
  • 17
  • He is doing that `b1 = b1.subtract(b2);` – Austin Mar 15 '13 at 09:24
  • No problem! After the edit of the question you have a problem experiencing the issue of limited precision in floating-point numbers, which this answer obviously does not fix:) – ghdalum Mar 15 '13 at 09:35
2

So the real question is: with the following code,

BigDecimal b1 = new BigDecimal(0.01);
BigDecimal b2 = new BigDecimal(0.01);
b1 = b1.subtract(b2);

why does b1.toString() evaluate to "0E-59" and not to something like "0.0", "0E0" or just "0"?


The reason is that toString() prints the canonical format of the BigDecimal. See BigDecimal.toString() for more information.

At the end, 0E-59 is 0.0 - it is 0*10^59 which mathematically evaluates to 0. So, the unexpected result is a matter of the internal representation of the BigDecimal.

To get the float or double values, use

b1.floatValue());

or

b1.doubleValue());

Both evaluate to 0.0.

Andreas Fester
  • 36,091
  • 7
  • 95
  • 123
  • But again, I would expect b1 and b2 to be exactly the same, neither of them exactly 0.01. So the subtraction should yield exactly 0. – paxdiablo Mar 15 '13 at 09:29
2

BigDecimal(double val)

1.The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

2.The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal("0.1") creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.

3.When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor. To get that result, use the static valueOf(double) method.

Achintya Jha
  • 12,735
  • 2
  • 27
  • 39
1

It's a known issue, BigDecimal(double val) API The results of this constructor can be somewhat unpredictable. Though it looks really wierd in this interpertation. Actual reason is that new BigDecimal(0.01) produces a BigDecimal with approx values

0.01000000000000000020816681711721685132943093776702880859375

which has a long precision, and so the result of subtract has a long precision too.

Anyway, we can solves the "problem" this way

BigDecimal b1 = new BigDecimal("0.01");
BigDecimal b2 = new BigDecimal("0.01");

or we can use a constructor with setting a precision

BigDecimal b1 = new BigDecimal(0.01, new MathContext(1));
BigDecimal b2 = new BigDecimal(0.01, new MathContext(1));
Evgeniy Dorofeev
  • 133,369
  • 30
  • 199
  • 275
1

Use like this:

BigDecimal b1 = BigDecimal.valueOf(0.01);
BigDecimal b2 = BigDecimal.valueOf(0.01);

b1 = b1.subtract(b2);
System.out.println("BigDecimal result: "+ b1);
currarpickt
  • 2,290
  • 4
  • 24
  • 39