Как избежать ошибок точности с плавающей запятой с плавающей запятой или удваивается в Java? - PullRequest
30 голосов
/ 10 марта 2011

У меня очень раздражающая проблема с длинными суммами чисел с плавающей точкой или двойных чисел в 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

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

Есть идеи?

Ответы [ 12 ]

33 голосов
/ 10 марта 2011

Нет точного представления 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

9 голосов
/ 10 марта 2011

Вы можете избежать этой конкретной проблемы, используя такие классы, как BigDecimal. float и double, будучи плавающей точкой IEEE 754, не предназначены для обеспечения идеальной точности, они предназначены для быстрой работы. Но обратите внимание на следующее замечание Джона: BigDecimal не может точно представлять «одну треть», более чем double может точно представлять «одну десятую». Но для (скажем) финансовых расчетов, BigDecimal и подобных им классов, как правило, путь, потому что они могут представлять числа так, как мы, люди, склонны думать о них.

7 голосов
/ 10 марта 2011

Не используйте 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 символ

Кстати: не используйте float, если нет действительно веских причин не использовать double.

4 голосов
/ 10 марта 2011

Это , а не , просто накопленная ошибка (и абсолютно не связанная с Java). 1.0f после перевода в реальный код не имеет значения 0,1 - вы уже получаете ошибку округления.

С Гид с плавающей точкой:

Что я могу сделать, чтобы избежать этой проблемы?

Это зависит от того, какого рода расчеты вы делаете.

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

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

3 голосов
/ 17 ноября 2012

Для полноты картины я рекомендую следующее:

Shewchuck, "Надежные адаптивные геометрические предикаты с плавающей запятой", если вам нужно больше примеров того, как выполнять точную арифметику с плавающей запятой - или хотя бы контролироватьточность, которая является первоначальным намерением автора, http://www.cs.berkeley.edu/~jrs/papers/robustr.pdf

2 голосов
/ 12 мая 2016
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));
    }
}
2 голосов
/ 24 июля 2015

Я столкнулся с той же проблемой, решил то же самое с помощью 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);

Надеюсь, это поможет вам.

2 голосов
/ 10 марта 2011

Другое решение состоит в том, чтобы отказаться от == и проверить, достаточно ли два значения достаточно близко .(Я знаю, что это не то, что вы спросили в теле, но я отвечаю на заголовок вопроса.)

2 голосов
/ 10 марта 2011

Вы должны использовать десятичный тип данных, а не с плавающей точкой:

https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html

1 голос
/ 13 апреля 2018

Сначала сделайте это double .Никогда не используйте float , иначе у вас возникнут проблемы с утилитами java.lang.Math.

Теперь, если вам случится заранее знать точность , которую вы хотите, и эторавен или меньше 15, тогда вашим 1010 * двойным s становится легко сказать, как себя вести.Проверьте ниже:

// 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;
}

Теперь, когда вы выполняете операцию, вы должны указать своему double результату поведения:

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

NOTE1: Для производительности вам следуеткэшировать операцию Math.pow в массиве.Здесь не сделано для ясности.

NOTE2: Именно поэтому мы никогда не используем double s для цен, но long s, где последний N (т.е. где N <= 15, обычно 8) цифр - десятичные цифры.Тогда вы можете забыть о том, что я написал выше:) </p>

...