float к двойному назначению
Рассмотрим следующий фрагмент кода
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
не может быть представлено точно Как число с плавающей точкой в PC. это происходит потому, что числа с плавающей запятой представлены в виде суммы двоичных дробей в ПК.0.5
,0.25
и0.125
можно точно преобразовать, но не0.583
.Поплавки (и двойники) представляются в виде
Σ( 1/2^i*Bi )
, гдеBi
-i-й бит(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 float.С самими поплавками это замалчивается, потому что
println
отпечаткиСтолько же, но только столько же, больше цифр как необходимы, чтобы однозначно различать значение аргумента из соседних значения типа float.
В во многих случаях это означает, что десятичное значение литерала будет напечатано. Однако, когда вы присваиваете значение
double
, "соседние значения типа double" обычно намного, намного ближе, чем значения типаfloat
, поэтому вы можете увидеть истинное значение вашего аппроксимированного float.Для получения более подробной информации прочитайте руководство с плавающей запятой.
Приближение существует все время. Просто так получилось, что двойник дает достаточно дополнительных битов, что появляется дополнительный материал.
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 цифры меньше, чем точность float.
Насколько я понимаю, ваша забота заключается в том, почему этот код...
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
печатается.
Короче говоря, не используйте 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
Поплавки и двойники на самом деле имеют одно и то же значение внутри; они просто напечатаны по-разному. Добавьте эти строки в свою программу, чтобы просмотреть их в шестнадцатеричном формате:
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.
Теперь 0х1.19954p8 = 100011001.100101010100 = 281.5830078125, и 0x1.19947ap8 = 100011001.1001010001111010 = 281.579986572265625. Все, что происходит, это то, что они округляются по-разному при печати (поплавки округляются до меньшее количество цифр, чем удваивается).