Как избежать ошибок точности с плавающей запятой с помощью float или double в Java?
У меня очень неприятная проблема с длинными суммами поплавков или удвоений в Java. По сути, идея заключается в том, что если я выполняю:
for ( float value = 0.0f; value < 1.0f; value += 0.1f )
System.out.println( value );
Я получаю:
0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.70000005
0.8000001
0.9000001
Я понимаю, что есть накопление ошибки с плавающей точностью, однако, как избавиться от этого? Я попытался использовать удвоение до половины ошибки, но результат все тот же.
Любые идеи?
Ответы
Ответ 1
Нет точного представления 0,1 как float
или double
. Из-за этой ошибки представления результаты немного отличаются от ожидаемых.
Несколько подходов, которые вы можете использовать:
- При использовании типа
double
отобразите только столько цифр, сколько вам нужно. Когда проверка на равенство допускает небольшой допуск в любом случае.
- В качестве альтернативы используйте тип, который позволяет вам хранить числа, которые вы пытаетесь точно представлять, например
BigDecimal
может точно представлять 0,1.
Пример кода для BigDecimal
:
BigDecimal step = new BigDecimal("0.1");
for (BigDecimal value = BigDecimal.ZERO;
value.compareTo(BigDecimal.ONE) < 0;
value = value.add(step)) {
System.out.println(value);
}
Смотрите онлайн: ideone
Ответ 2
Вы можете избежать этой конкретной проблемы, используя классы, такие как BigDecimal
. float
и double
, являющиеся плавающей точкой IEEE 754, не предназначены для обеспечения точности, они рассчитаны на быструю работу. Но обратите внимание на то, что Джон ниже: BigDecimal
не может точно представлять "одну треть" точно, не более double
может точно представлять "одну десятую". Но для (скажем) финансовых расчетов BigDecimal
и классов, как это обычно бывает, потому что они могут представлять числа так, как мы, люди, склонны думать о них.
Ответ 3
Не используйте float/double в итераторе, так как это максимизирует вашу ошибку округления. Если вы просто используете следующие
for (int i = 0; i < 10; i++)
System.out.println(i / 10.0);
он печатает
0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
Я знаю, что BigDecimal - это популярный выбор, но я предпочитаю двойной не потому, что он намного быстрее, но его обычно намного короче/чище, чтобы понять.
Если вы считаете количество символов в качестве меры сложности кода
- с использованием double = > 11 символов
- используйте BigDecimal (из примера @Mark Byers) = > 21 символ
BTW: не используйте float, если нет веской причины не использовать double.
Ответ 4
Это не просто накопившаяся ошибка (и не имеет абсолютно никакого отношения к Java). 1.0f
, после перевода в фактический код, не имеет значения 0,1 - вы уже получаете ошибку округления.
Из Руководство по плавающей запятой:
Что я могу сделать, чтобы избежать этой проблемы?
Это зависит от того, расчеты, которые вы делаете.
- Если вам действительно нужны ваши результаты, чтобы их точно добавить, особенно когда вы работаете с деньгами: используйте специальный десятичный тип данных.
- Если вы просто не хотите видеть все эти дополнительные десятичные знаки: просто отформатируйте результат, округленный до фиксированного количество знаков после запятой, когда показывая его.
- Если у вас нет десятичного типа данных, альтернативой является работа с целыми числами, например. делать деньги расчеты полностью в центах. Но это больше работы и имеет некоторые недостатки.
Прочтите ссылку на сайт для получения более подробной информации.
Ответ 5
Для полноты я рекомендую это:
Shewchuck, "Robust Adaptive Floating-Point Geometric Predicates", если вы хотите больше примеров того, как выполнять точную арифметику с плавающей точкой или, по крайней мере, контролируемой точности, которая является первоначальным намерением автора, http://www.cs.berkeley.edu/~jrs/papers/robustr.pdf
Ответ 6
Вы должны использовать десятичный тип данных, а не плавать:
https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html
Ответ 7
Другое решение - отказаться от ==
и проверить, достаточно ли близки два значения. (Я знаю, что это не то, что вы просили в теле, но я отвечаю на вопрос.)
Ответ 8
Я столкнулся с такой же проблемой, решив то же самое с помощью BigDecimal. Ниже приведен фрагмент, который помог мне.
double[] array = {45.34d, 45000.24d, 15000.12d, 4534.89d, 3444.12d, 12000.00d, 4900.00d, 1800.01d};
double total = 0.00d;
BigDecimal bTotal = new BigDecimal(0.0+"");
for(int i = 0;i < array.length; i++) {
total += (double)array[i];
bTotal = bTotal.add(new BigDecimal(array[i] +""));
}
System.out.println(total);
System.out.println(bTotal);
Надеюсь, это поможет вам.
Ответ 9
package loopinamdar;
import java.text.DecimalFormat;
public class loopinam {
static DecimalFormat valueFormat = new DecimalFormat("0.0");
public static void main(String[] args) {
for (float value = 0.0f; value < 1.0f; value += 0.1f)
System.out.println("" + valueFormat.format(value));
}
}
Ответ 10
Сначала сделайте это двойным. Никогда не используйте float или у вас возникнут проблемы с использованием утилиты java.lang.Math
.
Теперь, если вы заранее знаете точность, которую хотите, и она равна или меньше 15, тогда становится легче сказать, что ваши парные дужки ведут себя. Проверьте ниже:
// the magic method:
public final static double makePrecise(double value, int precision) {
double pow = Math.pow(10, precision);
long powValue = Math.round(pow * value);
return powValue / pow;
}
Теперь, когда вы делаете операцию, вы должны сообщить вашему двойному результату:
for ( double value = 0.0d; value < 1.0d; value += 0.1d )
System.out.println( makePrecise(value, 1) + " => " + value );
Вывод:
0.0 => 0.0
0.1 => 0.1
0.2 => 0.2
0.3 => 0.30000000000000004
0.4 => 0.4
0.5 => 0.5
0.6 => 0.6
0.7 => 0.7
0.8 => 0.7999999999999999
0.9 => 0.8999999999999999
1.0 => 0.9999999999999999
Если вам нужно более 15 очков, вам не повезло:
for ( double value = 0.0d; value < 1.0d; value += 0.1d )
System.out.println( makePrecise(value, 16) + " => " + value );
Вывод:
0.0 => 0.0
0.1 => 0.1
0.2 => 0.2
0.3000000000000001 => 0.30000000000000004
0.4 => 0.4
0.5 => 0.5
0.6 => 0.6
0.7 => 0.7
0.8 => 0.7999999999999999
0.9 => 0.8999999999999999
0.9999999999999998 => 0.9999999999999999
ПРИМЕЧАНИЕ 1. Для производительности вы должны кэшировать операцию Math.pow
в массиве. Здесь не делается ясности.
ПРИМЕЧАНИЕ 2. Это то, что мы никогда не используем двойные цены, но долготы, где последние N (т. Е. Где N <= 15, обычно 8) цифры являются десятичными цифрами. Тогда вы можете забыть о том, что я написал выше :)
Ответ 11
Если вы хотите продолжать использовать float
и избегать накопления ошибок, повторно добавляя 0.1f
, попробуйте что-то вроде этого:
for (int count = 0; count < 10; count++) {
float value = 0.1f * count;
System.out.println(value);
}
Обратите внимание, однако, как уже объяснили другие, float
не является бесконечно точным типом данных.
Ответ 12
Вам просто нужно быть в курсе точности, необходимой для вашего расчета, и точности, которую ваш выбранный тип данных способен и соответствует вашим ответам.
Например, если вы имеете дело с цифрами с 3 значащими цифрами, целесообразно использовать float
(который обеспечивает точность 7 значительных цифр). Однако вы не можете процитировать свой окончательный ответ с точностью до 7 значащих цифр, если ваши начальные значения имеют только 2 значимые цифры.
5.01 + 4.02 = 9.03 (to 3 significant figures)
В вашем примере вы выполняете множество дополнений, и с каждым добавлением происходит последующее влияние на конечную точность.