0

I am trying to understand floating-point numbers more deeply. I know that binary (base 2) floating-point numbers cannot represent some decimal numbers exactly. But I am confused in this strange behavior of double in Java (in Java, double is a binary floating-point number data type).

I write this piece of Java code:

public void myTestMethod() {
    double num;
    double factor;
    double compared;
    double result1;
    double result2;
    String output;

    num = 0.3;
    factor = 10;
    compared = 3;

    //num * 10
    result1 = num * factor;

    //add 10 num, which is mathematically equal to num * 10
    result2 = num + num + num + num + num + num + num + num + num + num;

    output = "num: " + num + "\n";
    output = output + "result1: " + result1 + "\n";
    output = output + "result2: " + result2 + "\n";

    if (result1 == compared) {
        output = output + "result1 == compared\n";
    } else {
        output = output + "result1 != compared\n";
    }

    if (result2 == compared) {
        output = output + "result2 == compared\n";
    } else {
        output = output + "result2 != compared\n";
    }

    System.out.print(output);
}

Running this method produces this output:

num: 0.3
result1: 3.0
result2: 2.9999999999999996
result1 == compared
result2 != compared

From what I know, the value of "factor" is exactly 10, and the value of "compared" is exactly 3, because these integers can be represented exactly by double. But maybe the value of "num" is a little different from 0.3.

Now I want to know:

  1. Is the value of "num" 0.3 (exactly)?

  2. If the value of "num" is 0.3, then, why is "result2 == compared" false?

  3. If the value of "num" is not 0.3, then, why is its String output "0.3"? From what I know, binary floating-point numbers can always be represented exactly in decimal numbers. So, if the value of "num" is not 0.3, its String output should not be "0.3", but should be something like "0.300000000001".

  4. If the value of "num" is not 0.3, then, why is "result1 == compared" true?

  5. If the value of "num" is not 0.3, then, what is it?

zhoudu
  • 623
  • 8
  • 19
  • 1
    you should never compare double primitive types using '==' – ΦXocę 웃 Пepeúpa ツ May 26 '23 at 07:50
  • Here's an interesting [link](https://www.baeldung.com/java-comparing-doubles) for you. It explains why you cant use '''=='''' – fnymmm May 26 '23 at 07:52
  • Yeah, I know that in most cases I should not use "==" for floating-point numbers, because they are often not exact. But here I use "==", in order to understand floating-point numbers more deeply. – zhoudu May 26 '23 at 07:55
  • 6
    The strange result is a direct consequence of the thing that you say that you understand. If you want a deeper understanding, you need to understand the mathematics. One place to start is: [What Every Computer Scientist Should Know About Floating-Point Arithmetic](https://docs.oracle.com/cd/E19957-01/800-7895/800-7895.pdf). Read it, and then read it again ... until you understand it. – Stephen C May 26 '23 at 08:03
  • 2
    (1) No. Call `System.out.println(new BigDecimal(num))` to see the exact value of `num`. (2) Do the same for `result2`. (3) See the Javadoc of [`Double#toString(double)`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/Double.html#toString(double)). (4) Do the same as for question 1 and 2, but for `result1`. (5) Answered already. (Bonus) Read the link provided by Stephen to understand the results. – Slaw May 26 '23 at 08:14
  • @Slaw So, why is the direct String output of "num" not this? Is it because Java rounds it? But "result2" is not rounded. This is strange. – zhoudu May 26 '23 at 08:16
  • *"Is the value of "num" 0.3 (exactly)?"* No. As you say, *"binary (base 2) floating-point numbers cannot represent some decimal numbers exactly"*. This is an example of a number that cannot be exactly represented, because no integers `p` and `q` exist such that `p * 2^q = 0.3`. – slothrop May 26 '23 at 08:33
  • Floating-point arithmetic does *not* always obey the laws of mathematics. a + a + a + … + a is *not* always equal to a × N, due to rounding. (Addition isn't associative, either: (a + b) + c may be different from a + (b + c).) – Steve Summit May 26 '23 at 13:31
  • You thought you knew that binary floating-point numbers cannot represent some (really most) decimal numbers exactly. But you have not yet understood all of the implications of this fact. You thought you were computing things like a + b. But you were *actually* computing a' ⊕ b', where a' and b' are the binary approximations to a and b, and where ⊕ is floating-point addition, which is slightly different than mathematical addition. Furthermore, after computing a result, Java converted it back to decimal for you, resulting in another approximation. – Steve Summit May 26 '23 at 13:40
  • Bottom line, your comment `add 10 num, which is mathematically equal to num * 10` is perfectly false, for finite-precision floating point. (So in this case your use of `==` on floating-point values was not wrong, and it did help you understand floating-point numbers more deeply, because the results really weren't equal!) – Steve Summit May 26 '23 at 13:42

3 Answers3

7
  1. Is the value of "num" 0.3 (exactly)?

No, it is 0.299999999999999988897769753748434595763683319091796875.

  1. If the value of "num" is 0.3, then, why is "result2 == compared" false?

The premise is false; the value of num is not 0.3. Further, result2 is computed using nine additions. In each addition, there is a rounding step. The result of these additions with rounding is 2.999999999999999555910790149937383830547332763671875.

  1. If the value of "num" is not 0.3, then, why is its String output "0.3"? From what I know, binary floating-point numbers can always be represented exactly in decimal numbers. So, if the value of "num" is not 0.3, its String output should not be "0.3", but should be something like "0.300000000001".

The Java specifiation says that the default formatting for a double is to print just enough decimal digits to uniquely distinguish the number. Specifically, it converts to the decimal numeral with the fewest digits having the property that converting the decimal numeral back to the double type yields the original value.

Converting 0.3 to double produces the nearest representable value, 0.299999999999999988897769753748434595763683319091796875, so, when printing 0.299999999999999988897769753748434595763683319091796875. with default formatting, Java produces “0.3”.

  1. If the value of "num" is not 0.3, then, why is "result1 == compared" true?

num is 0.299999999999999988897769753748434595763683319091796875. Multiplying it by 10 would produce 2.99999999999999988897769753748434595763683319091796875 using real-number arithmetic, but this number is not representable in double. The floating-point multiplication operation produces the nearest value representable in double, which is 3.

  1. If the value of "num" is not 0.3, then, what is it?

It is 0.299999999999999988897769753748434595763683319091796875.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
1

To see what's going on here, let's do a similar problem in decimal, instead — that is, not involving binary floating-point at all. We're going to use decimal floating point, limited to three significant digits of precision. We're going to start with the value 1/3, and we're going to multiply it by 10, which should give us 10/3.

But of course, we can't represent 1/3 exactly in decimal. The closest we can get, with 3 significant digits, is 0.333. But that's pretty close.

If we take 0.333, and multiply it by 10, we obviously get 3.33. That's not quite the same as 10/3 (which would be 3.33333…), but again, it's pretty close.

But suppose we take 0.333 and add it to itself 10 times. That should give the same answer, right?

0.333 + 0.333 + 0.333 is 0.999. All right so far.

0.999 + 0.333 should be 1.332. But we've only got three significant digits of precision. So we're going to have to round that off to 1.33.

1.33 + 0.333 should be 1.663. But we've only got three significant digits of precision. So we're going to have to round that off to 1.66.

1.66 + 0.333 should be 1.993. But we're going to have to round that off to 1.99.

(Are you beginning to see a pattern? It's about to get a little worse.)

1.99 + 0.333 should be 2.323, which rounds off to 2.32.
2.32 + 0.333 should be 2.653, which rounds off to 2.65.
2.65 + 0.333 should be 2.983, which rounds off to 2.98.
Finally, 2.98 + 0.333 should be 3.313, which rounds off to 3.31.

So 0.333 × 10 was 3.33, but 0.333 + 0.333 + 0.333 + … + 0.333 is 3.31. Why did we get two different answers?

Because just about every time we added 0.333, we had to round off the result, and (in this case) that always meant throwing away 0.003. And after seven addition steps which rounded, those little roundoff errors added up to something that significantly affected the final result.

And more or less exactly the same thing goes on when you repeatedly add 0.3 in binary. 0.3 is not exactly representable in binary, so you weren't adding exactly 0.3 each time. You were adding a value slightly different than 0.3, and after nine additions, the roundoff errors added up to something significantly different than when you just multiplied 0.3 by 10. See Eric Postpischil's answer for more details.

Steve Summit
  • 45,437
  • 7
  • 70
  • 103
0

Since the range of all decimal numbers is infinite and any data type has finite space, you cannot represent all possible decimal numbers using a double data type. That's why some numbers will be rounded to their next possible representation.

For more details on the implementation, check the IEEE 754 standard.

m0skit0
  • 25,268
  • 11
  • 79
  • 127