Как избежать ошибок точности с плавающей запятой с помощью 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

Ответ 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)

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