Преобразуйте float в double без потери точности


97

У меня есть примитивный поплавок и мне нужен как примитив-двойник. Простое преобразование float в double дает мне странную дополнительную точность. Например:

float temp = 14009.35F;
System.out.println(Float.toString(temp)); // Prints 14009.35
System.out.println(Double.toString((double)temp)); // Prints 14009.349609375

Однако, если вместо приведения я выведу число с плавающей запятой как строку и проанализирую строку как двойную, я получу то, что хочу:

System.out.println(Double.toString(Double.parseDouble(Float.toString(temp))));
// Prints 14009.35

Есть ли лучший способ, чем перейти к String и обратно?

Ответы:


125

Дело не в том, что на самом деле вы получаете дополнительную точность - дело в том, что поплавок не точно отражает число, к которому вы изначально стремились. Дважды в точно представляющий оригинальный поплавок; toStringпоказывает "лишние" данные, которые уже были.

Например (и эти числа неверны, я просто придумываю) предположим, что у вас было:

float f = 0.1F;
double d = f;

Тогда значение fможет быть точно 0,100000234523. dбудет иметь точно такое же значение, но когда вы преобразуете его в строку, он будет «доверять», что он точен до более высокой точности, поэтому не округлится так рано, и вы увидите «лишние цифры», которые уже были есть, но скрыто от вас.

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

Вы уверены, что подходящие типы float / double вместо BigDecimal? Если вы пытаетесь использовать числа с точными десятичными значениями (например, деньги), то BigDecimalэто более подходящий тип IMO.


40

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

float f = 0.27f;
double d2 = (double) f;
double d3 = 0.27d;

System.out.println(Integer.toBinaryString(Float.floatToRawIntBits(f)));
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(d2)));
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(d3)));

Вы можете видеть, что число с плавающей запятой расширяется до двойного за счет добавления 0 в конец, но двойное представление 0,27 «более точное», отсюда и проблема.

   111110100010100011110101110001
11111111010001010001111010111000100000000000000000000000000000
11111111010001010001111010111000010100011110101110000101001000

25

Это связано с контрактом Float.toString(float), в котором частично говорится:

Сколько цифр нужно напечатать для дробной части […]? Там должно быть по крайней мере одна цифра для представления дробной части, и за ее пределами , что , как многие, но только как многие, больше цифр, которые необходимы , чтобы однозначно отличить значение аргумента из соседних значений типа поплавка. То есть предположим, что x - точное математическое значение, представленное десятичным представлением, полученным этим методом для конечного ненулевого аргумента f. Тогда f должно быть ближайшим к x значением с плавающей запятой; или, если два значения с плавающей запятой одинаково близки к x, тогда f должен быть одним из них, а младший бит мантиссы f должен быть 0.


13

Я столкнулся с этой проблемой сегодня и не смог использовать рефакторинг до BigDecimal, потому что проект действительно огромен. Однако я нашел решение, используя

Float result = new Float(5623.23)
Double doubleResult = new FloatingDecimal(result.floatValue()).doubleValue()

И это работает.

Обратите внимание, что вызов result.doubleValue () возвращает 5623.22998046875

Но вызов doubleResult.doubleValue () возвращает правильно 5623.23

Но я не совсем уверен, правильное ли это решение.


1
Работал у меня. Также очень быстро. Не уверен, почему это не помечено как ответ.
Sameer

Это метод, который я использую, и вам не нужна новая часть Float ... Просто создайте FloatingDecimal с примитивом. Он будет автоматически упакован ... И работает также быстро ... Есть один недостаток, FloatingDecimal является неизменным, поэтому вам нужно создать его для каждого отдельного числа с плавающей запятой ... представьте себе вычислительный код O (1e10)! !! Конечно, у BigDecimal есть тот же недостаток ...
Мостафа Зейнали

6
Недостатком этого решения является то, что sun.misc.FloatingDecimal является внутренним классом JVM, и подпись его конструктора была изменена в Java 1.8. Никто не должен использовать внутренние классы в реальных приложениях.
Игорь Бляхин

8

Я нашел следующее решение:

public static Double getFloatAsDouble(Float fValue) {
    return Double.valueOf(fValue.toString());
}

Если вы используете float и double вместо Float и Double, используйте следующее:

public static double getFloatAsDouble(float value) {
    return Double.valueOf(Float.valueOf(value).toString()).doubleValue();
}

7

Используйте BigDecimalвместо float/ double. Есть много чисел, которые нельзя представить как двоичные числа с плавающей запятой (например, 0.1). Таким образом, вы должны либо всегда округлять результат до известной точности, либо использоватьBigDecimal .

См. Http://en.wikipedia.org/wiki/Floating_point для получения дополнительной информации.


Допустим, у вас в кеше есть 10-миллионные торговые цены с плавающей запятой, вы все еще думаете об использовании BigDecimal и создаете уникальные, скажем, ~ 6 миллионов объектов (для скидки на легковесные / неизменяемые) .. или есть еще кое-что?
Sendi_t

1
@Sendi_t Пожалуйста, не используйте комментарии, чтобы задавать сложные вопросы :-)
Аарон Дигулла,

Спасибо, что посмотрели! ... мы используем поплавки прямо сейчас, как это было согласовано десять лет назад - я пытался понять вашу точку зрения на эту ситуацию в дальнейшем -. Спасибо!
Sendi_t

@Sendi_t Недоразумение. Я не могу ответить на ваш вопрос в 512 знаков. Пожалуйста, задайте правильный вопрос и пришлите мне ссылку.
Аарон Дигулла

1
@GKFX Когда дело доходит до обработки десятичных дробей на компьютерах, не существует "универсального решения". Но поскольку большинство людей этого не понимают, я указываю им на это, BigDecimalтак как это позволит отловить большинство распространенных ошибок. Если дела идут слишком медленно, им нужно узнать больше и найти способы оптимизировать свои проблемы. Преждевременная оптимизация - корень всех зол, - Д.Е. Кнут.
Аарон Дигулла

1

Поплавки по своей природе неточны и всегда имеют аккуратные "проблемы" округления. Если точность важна, вы можете подумать о рефакторинге своего приложения для использования Decimal или BigDecimal.

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


1
Десятичная арифметика тоже неточна. (например, 1/3 * 3 == 0,99999999999999999999999999) Это, конечно, лучше для представления точных десятичных величин, таких как деньги, но для физических измерений это не имеет преимуществ.
dan04

2
Но 1 == 0,9999999999999999999999999999 :)
Эммануэль Бург,

0

Информацию можно найти в разделе 48 - Избегайте float и double, когда требуются точные значения, Эффективной Java 2-го издания Джошуа Блоха. В этой книге много хороших материалов, и ее определенно стоит посмотреть.



0

Простое решение, которое хорошо работает, - это проанализировать двойное значение из строкового представления числа с плавающей запятой:

double val = Double.valueOf(String.valueOf(yourFloat));

Не очень эффективно, но работает!

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.