Плавать, чтобы удвоить назначение

Рассмотрим следующий фрагмент кода

float num = 281.583f;
int amount = (int) Math.round(num*100f);
float rounded = amount/100.0f;
double dblPrecision = rounded;
double dblPrecision2 = num;
System.out.println("num : " + num + " amount: " + amount + " rounded: " + rounded + " dbl: " + dblPrecision + " dbl2: " + dblPrecision2);

Я получаю вывод

num : 281.583 amount: 28158 rounded: 281.58 dbl: 281.5799865722656 dbl2: 281.5830078125

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

6 ответов

Решение

Аппроксимация на самом деле имеет место при преобразовании десятичной дроби в float, Я могу удивить вас, но 281.583 не может быть представлен точно как число с плавающей точкой в ​​ПК. это происходит потому, что числа с плавающей запятой представляются в виде суммы двоичных дробей в ПК. 0.5, 0.25 а также 0.125 может быть преобразован точно, но не 0.583,

Поплавки (и двойники) представлены как Σ( 1/2^i*Bi ), где Bi это немного (0|1), 0.625 = 1/2 + 1/4 например. Проблема в том, что не все десятичные дроби могут быть преобразованы в конечную сумму двоичных дробей.

Вот как преобразуется это число (первая строка - определение столбца).

i|  *2 and trim|    Bit value|  (2^-1)*bit
    0,583       
1   1,166   1   0,5
2   0,332   0   0
3   0,664   0   0
4   1,328   1   0,0625
5   0,656   0   0
6   1,312   1   0,015625
7   0,624   0   0
8   1,248   1   0,00390625
9   0,496   0   0
10  0,992   0   0
11  1,984   1   0,000488281
12  1,968   1   0,000244141
13  1,936   1   0,00012207
14  1,872   1   6,10352E-05
15  1,744   1   3,05176E-05
16  1,488   1   1,52588E-05
17  0,976   0   0
18  1,952   1   3,8147E-06
19  1,904   1   1,90735E-06
        SUM=    0,582998276

Потому что числа с плавающей запятой - это двоичные дроби, поэтому они могут только приблизительно представлять ваше десятичное число. Приближение происходит, когда буквальное 281.583f в исходном коде анализируется в плавающее значение IEEE 754.

С самими поплавками это затмевается, потому что println печать

столько, но только столько, сколько цифр необходимо, чтобы однозначно отличить значение аргумента от смежных значений типа float.

Во многих случаях это означает, что будет напечатано десятичное значение литерала. Тем не менее, когда вы присваиваете значение double"смежные значения типа double" обычно намного, намного ближе, чем значения типа float, так что вы можете увидеть истинную стоимость вашего приблизительного числа с плавающей точкой.

Для получения более подробной информации читайте Руководство с плавающей точкой.

Короче говоря, не используйте float, если вам не нужно. Вы потеряете точность и, скорее всего, сэкономите очень мало. Используйте двойной, и вы сэкономите много горя.

double num = 281.583;
long amount = (long) (num*100);
double rounded = (double) amount/100;
double dblPrecision = rounded;
double dblPrecision2 = num;

печать

num : 281.583 amount: 28158 rounded: 281.58 dbl: 281.58 dbl2: 281.583

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

281.583, например, в двоичном формате (с большим количеством цифр, но с двойной точностью): 100011001.1001_0101_0011_1111_0111_1100_1110_1101_1001...

Float допускает около 23 бит, а double - около 52 бит. (Не могу вспомнить точно) 100011001.1001_0101_0011_11, что составляет 281,582946777 в десятичном виде.

Для справки: с одинарной точностью сохраняется до 7 десятичных цифр, а с двойной точностью - до 16 десятичных. Это включает в себя все числа, так что у вас только примерно на 1 цифру меньше, чем точность с плавающей точкой.

Плавающие и двойные на самом деле имеют одинаковое значение внутри; они просто напечатаны по-разному. Добавьте эти строки в вашу программу, чтобы просмотреть их в шестнадцатеричном виде:

System.out.printf("num:           %a\n",num);
System.out.printf("dblPrecision2: %a\n",dblPrecision2);

System.out.printf("rounded:       %a\n",rounded);
System.out.printf("dblPrecision:  %a\n",dblPrecision);

Это печатает

num:           0x1.19954p8
dblPrecision2: 0x1.19954p8
rounded:       0x1.19947ap8
dblPrecision:  0x1.19947ap8

num = dblPrecision2 и округлено = dblPrecision.

Теперь 0x1.19954p8 = 100011001.100101010100 = 281,5830078125 и 0x1.19947ap8 = 100011001.1001010001111010 = 281,579986572265625. Все, что происходит, это то, что при печати они округляются по-разному (числа с плавающей точкой округляются до меньшего числа цифр, чем двойных).

Насколько я понимаю, вы беспокоитесь, почему этот код...

float f = 281.583f;
System.out.println(f);
System.out.println((double) f);

... печать

281.583
281.5830078125

(эй, double обеспечивает большую точность!)

Вот почему...

Кормить 438ccaa0 (шестнадцатеричный формат битов, представляющих 281.583f как указано Integer.toHexString(Float.floatToRawIntBits(281.583f))) в форму здесь. Что вы увидите, так это то, что поплавок на самом деле представлен как 281.58301, (@Michael Borgwardt отвечает, почему это так не печатается.)

Так 281.583 печатается для 281.58301 когда представлено как поплавок. Но когда вы конвертируете 281.58301 в два раза, вы можете стать ближе к 281.58301 чем 281.583!

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

Другие вопросы по тегам