Почему два результата с плавающей запятой отличаются, когда они номинально имеют одинаковое значение?

недавно я читал о хранении значений с плавающей запятой в памяти. И я написал небольшую программу, чтобы проверить то, что я прочитал. И я заметил, что есть разница в том, как Java обрабатывает значения с плавающей запятой.

public class Test
{
   public static void main(String args[])
   {
     double a = 0.90;
     System.out.println(a);
     System.out.println(2.00-1.10);
   }
 }

вышеуказанная программа печатает

0.9
0.8999999999999999

почему оба эти оператора не печатают одно и то же значение? Я знаю, что некоторые плавающие значения не могут быть представлены точно. В таком случае оба должны иметь одинаковую ценность.

4 ответов


когда" 0.90 " преобразуется в double, результат .9 плюс небольшая ошибка, e0. Таким образом a равных .9+e0.

когда "1.10" преобразуется в double, результат 1.1 плюс небольшая ошибка, e1, так что результат 1.1+e1.

эти две ошибки, e0 и e1, как правило, не связаны друг с другом. Проще говоря, разные десятичные числа находятся на разных расстояниях от двоичные числа с плавающей запятой. При оценке 2.00-1.10, результат 2 - (1.1+e1) = .9–е1. Так что один из ваших номеров .9+e0 и другие .9-е1, и нет причин ожидать, что они будут одинаковыми.

(Как это происходит в этом случае, е0 есть .00000000000000002220446049250313080847263336181640625, и Е1 есть .000000000000000088817841970012523233890533447265625. Кроме того, вычитание 1.1 из 2 вводит никаких новых ошибок, после преобразования "1.1" в два раза, по Лемме Штербенц’.)

дополнительная информация:

в двоичном, .9 is .11100110011001100110011001100110011001100110011001100 11001100... биты в полужирный вписывается в двойной. Младшие биты не подходят, поэтому число округляется в этой точке. Это вызывает разницу между точным значением .9 и значение ".9 " представлен в виде двойника. В двоичном формате 1.1 1.0001100110011001100110011001100110011001 10011001 ... опять же, число округляется. Но соблюдать величину округления разные. Для.9, 1100 1100 ... был округлен до 1 0000 0000..., что добавляет 00110011 ... в этой позиции. Для 1.1 1001 1001 округляется до 1 0000 0000..., что добавляет 01100110... в этом положении (и вызывает перенос жирным шрифтом). И эти две позиции различны; 1.1 начинается слева от точки радиуса, поэтому это выглядит так: 1.[52 бит вот!--44-->][ место, где происходит округление]. .9 начинается справа от точки радикса, так это выглядит так: .[ 53 бита здесь][ место, где происходит округление]. Таким образом, округление для 1.1, помимо того, что 01100110... вместо 00110011..., также удваивается, потому что оно происходит на один бит слева .9 округление. Таким образом, у вас есть два эффекта, делающих e0 отличается от e1: конечные биты, которые были округлены, отличаются и место, где происходит округление, отличается.


почему оба эти оператора не печатают одно и то же значение?

результат не то же самое.

Я знаю, что некоторые плавающие значения не могут быть представлены точно.

поэтому вы должны предположить, что результат операции может зависеть от величины ошибки представления используемых вами значений.

for (long l = 1; l <= 1e16; l *= 10) {
    double a = l + 2;
    double b = l + 1.1;
    System.out.println(a + " - " + b + " is " + (a - b));
}

по мере увеличения значения ошибка представления увеличивается и становится больше по сравнению с результат 0.9

3.0 - 2.1 is 0.8999999999999999
12.0 - 11.1 is 0.9000000000000004
102.0 - 101.1 is 0.9000000000000057
1002.0 - 1001.1 is 0.8999999999999773
10002.0 - 10001.1 is 0.8999999999996362
100002.0 - 100001.1 is 0.8999999999941792
1000002.0 - 1000001.1 is 0.9000000000232831
1.0000002E7 - 1.00000011E7 is 0.900000000372529
1.00000002E8 - 1.000000011E8 is 0.9000000059604645
1.000000002E9 - 1.0000000011E9 is 0.8999999761581421
1.0000000002E10 - 1.00000000011E10 is 0.8999996185302734
1.00000000002E11 - 1.000000000011E11 is 0.899993896484375
1.000000000002E12 - 1.0000000000011E12 is 0.9000244140625
1.0000000000002E13 - 1.00000000000011E13 is 0.900390625
1.00000000000002E14 - 1.000000000000011E14 is 0.90625
1.000000000000002E15 - 1.0000000000000011E15 is 0.875
1.0000000000000002E16 - 1.0000000000000002E16 is 0.0

и по теме, когда ошибка представления становится настолько большой, ваша операция ничего не делает.

for (double d = 1; d < Double.MAX_VALUE; d *= 2) {
    if (d == d + 1) {
        System.out.println(d + " + 1 == " + (d + 1));
        break;
    }
}
for (double d = 1; d < Double.MAX_VALUE; d *= 2) {
    if (d == d - 1) {
        System.out.println(d + " - 1 == " + (d - 1));
        break;
    }
}

печать

9.007199254740992E15 + 1 == 9.007199254740992E15
1.8014398509481984E16 - 1 == 1.8014398509481984E16

ваше рассуждение заключается в том, что, даже если 0.9 не может быть представлен точно double, что он должен иметь точно такой же double значение в 2.0 - 1.1, и поэтому результат в одной и той же стоимости. Это ошибка -- это вычитание не дает double в лице "0.9" (или точное значение 0,9).


Я знаю, что некоторые плавающие значения не могут быть представлены точно

Ну, это ваш ответ (или, точнее, как указал Марк Байерс, некоторые десятичные значения не могут быть представлены точно как двойной)! Ни 0.9, ни 1.1 не могут быть представлены как двойные, поэтому вы получаете ошибки округления.

вы можете проверить точное значение различных двойников с BigDecimal:

public static void main(String args[]) {
    double a = 0.9d;
    System.out.println(a);
    System.out.println(new BigDecimal(a));
    double b = 2d - 1.1d;
    System.out.println(b);
    System.out.println(new BigDecimal(2.0d));
    System.out.println(new BigDecimal(1.1d));
    System.out.println(new BigDecimal(b));
}

выходы:

0.9
0.90000000000000002220446049250313080847263336181640625
0.8999999999999999
2
1.100000000000000088817841970012523233890533447265625
0.899999999999999911182158029987476766109466552734375