Арифметика с плавающей точкой не дает точных результатов

Мне нужно выполнить некоторую арифметику с плавающей запятой в Java, как показано в приведенном ниже коде:

public class TestMain {
    private static Map<Integer, Double> ccc = new HashMap<Integer, Double>() {
      { put(1, 0.01); put(2, 0.02); put(3, 0.05); put(4, 0.1); put(6, 0.2);
        put(10, 0.5); put(20, 1.0); put(30, 2.0); put(50, 5.0); put(100, 10.0);
      }
    };

    Double increment(Double i, boolean up) {
        Double inc = null;

        while (inc == null) {
            inc = ccc.get(i.intValue());

            if (up)
                --i;
            else
                ++i;
        }
        return inc;
    }

    public static void main(String[] args) {
        TestMain tt = new TestMain();

        for (double i = 1; i < 1000; i += tt.increment(i, true)) {
            System.out.print(i + ",");
        }
    }
}

Это должно имитировать диапазон значений, указанный в качестве вывода виджет Spinner Betfair.

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

Ответы

Ответ 1

Если вам нужны точные десятичные значения, вы должны использовать java.math.BigDecimal. Затем прочитайте "Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой" на фоне того, почему вы получаете эти результаты.

(У меня есть . NET-ориентированная статья, которую вам может показаться проще читать - и, конечно, короче. Различия между Java и .NET в основном, не имеют отношения к пониманию этой проблемы.)

Ответ 2

Числа с плавающей запятой используют двоичные дроби, а не десятичные дроби. То есть вы используете десятичные дроби, состоящие из десятой цифры, сотой цифры, тысячной цифры и т.д. D1/10 + d2/100 + d3/1000... Но числа с плавающей запятой находятся в двоичном формате, поэтому они имеют половину цифры, четверть цифры, восьмую цифру и т.д. d1/2 + d2/4 + d3/8...

Многие десятичные дроби не могут быть точно выражены ни в одном конечном двоичном разряде. Например, 1/2 не проблема: в десятичном формате .5, в двоичном формате .1. 3/4 - десятичная .75, двоичная .11. Но 1/10 - это чистый .1 в десятичной форме, но в двоичном формате это .0001100110011... с "0011", повторяющимся навсегда. Поскольку компьютер может хранить только конечное число цифр, в какой-то момент это должно быть отрублено, поэтому ответ не является точным. Когда мы конвертируем обратно в десятичный знак на выходе, мы получаем странный номер.

Как говорит Джон Скит, если вам нужны точные десятичные дроби, используйте BigDecimal. Если производительность является проблемой, вы можете свернуть собственные десятичные дроби. Например, если вы знаете, что вы всегда хотите ровно 3 десятичных разряда и что число будет не более миллиона или около того, вы можете просто использовать int с предполагаемыми 3 десятичными знаками, внося необходимые коррективы, когда вы выполняете арифметику и записываете вывод чтобы вставить десятичную точку в нужное место. Но 99% времени производительность не является достаточно большой проблемой, чтобы стоить проблемы.

Ответ 3

Числа с плавающей запятой неточны, тем более, что они работают в двоичных фракциях (1/2, 1/4, 1/8, 1/16, 1/32,...) вместо десятичных дробей (1/10, 1/100, 1/1000,...). Просто определите, что вы чувствуете, "достаточно близко" и используйте что-то вроде Math.abs(a-b) < 0.000001.

Ответ 4

В философской заметке мне интересно: большинство компьютерных процессоров сегодня имеют встроенную поддержку целочисленной арифметики и арифметики с плавающей запятой, но не поддерживают десятичную арифметику. Почему нет? Я не писал приложение в те годы, когда поплавки можно было использовать из-за этой проблемы округления. Вы, конечно же, не можете использовать их за денежные суммы: никто не хочет печатать цену на квитанцию ​​о продаже "42.3200003 долларов". Ни один бухгалтер не согласится: "мы можем быть у вас ни копейки здесь и там, потому что мы используем двоичные дроби и имеем ошибки округления".

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

В те древние времена, когда я программировал на мэйнфреймах, семейство процессоров IBM 360 имело встроенную поддержку упакованной десятичной арифметики. Они сохраняли строки, в которых каждый байт содержал две десятичные цифры, т.е. Первые четыре бита имели значения от 0 до 9 и два вторых бита, а ЦП имел арифметические функции для их манипулирования. Почему Intel не может сделать что-то подобное? Тогда Java может добавить "десятичный" тип данных, и нам не понадобится весь лишний мусор.

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

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

Ответ 5

Вы можете сделать вывод своей программы более похожим на ожидаемый результат с помощью форматированного вывода.

http://java.sun.com/javase/6/docs/api/java/util/Formatter.html

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

Например, чтобы округлить результаты до двух знаков после запятой:

System.out.print(String.format(".2f", i) + ","); 

Ответ 6

Вы можете написать код для вычисления Epsilon на вашем компьютере. Я считаю, std:: С++ определяет его, его определяют другими способами в зависимости от того, что вы используете.

private static float calcEpsilonFloat() {
    float epsi = 1.0f;


    while ((float) (1.0 + (epsi / 2.0)) != 1.0)
    {
       epsi /= 2.0f;
    }

    return epsi;
}

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

Ответ 7

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

http://pastebin.com/CACER0xK

n - номер с большим количеством десятичных знаков (например, Math.PI), numberOfDecimals - максимальное количество десятичных знаков, которое вам нужно (например, 2 для 3.14 или 3 для 3.151).

По теории, положив отрицательное значение на numberOfDecmals, он также отрежет нижние целые цифры числа. Например, если положить n=1588.22 и numberOfDecimals=-2, функция вернет 1500.0.

Сообщите мне, если это что-то не так.