Как преодолеть неточность в Java


Я узнал о проблемах точности , когда выполнил следующую программу:

public static void main(String args[])
{
    double table[][] = new double[5][4];
    int i, j;
    for(i = 0, j = 0; i <= 90; i+= 15)
    {
        if(i == 15 || i == 75)
            continue;
        table[j][0] = i;
        double theta = StrictMath.toRadians((double)i);
        table[j][1] = StrictMath.sin(theta);
        table[j][2] = StrictMath.cos(theta);
        table[j++][3] = StrictMath.tan(theta);
    }
    System.out.println("angle#sin#cos#tan");
    for(i = 0; i < table.length; i++){
        for(j = 0; j < table[i].length; j++)
            System.out.print(table[i][j] + "t");
        System.out.println();
    }
}

И вывод:

angle#sin#cos#tan
0.0 0.0 1.0 0.0 
30.0    0.49999999999999994 0.8660254037844387  0.5773502691896257  
45.0    0.7071067811865475  0.7071067811865476  0.9999999999999999  
60.0    0.8660254037844386  0.5000000000000001  1.7320508075688767  
90.0    1.0 6.123233995736766E-17   1.633123935319537E16    

(прошу простить неорганизованный выход). Я отметил несколько вещей:

  • sin 30, то есть 0.5 хранится как 0.49999999999999994.
  • tan 45, то есть 1.0 хранится как 0.9999999999999999.
  • tan 90, т. е. infinity или undefined хранится как 1.633123935319537E16 (что является очень большим числом).

Естественно, я был совершенно сбит с толку, увидев вывод (даже после расшифровки вывода).

Итак, я прочитал этот пост , и лучший ответ говорит мне:

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

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

Ответил 7 октября 08 в 7: 42

Конрад Рудольф

Итак, мой вопрос:

Есть ли способ предотвратить Такие неточные результаты (в Java)?

Должен ли я округлить результаты? В этом случае, как я буду хранить infinity, то есть Double.POSITIVE_INFINITY?

3 2

3 ответа:

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

На практике это обычно означает делать такие вещи, как:

  • при отображении числа используйте String.format, чтобы указать количество точности для отображения (это сделает соответствующее округление для вас)
  • При сравнении с ожидаемым значением не ищите равенства (==). Вместо этого ищите достаточно маленькую дельту: Math.abs(myValue - expectedValue) <= someSmallError

EDIT: для бесконечности применим тот же принцип, но с поправкой: вы должны выбрать некоторое число, которое будет "достаточно большим", чтобы рассматривать его как бесконечность. Это опять же потому, что вы должны научиться жить с неточными ценностями, а не решать их. В случае чего-то вроде tan(90 градусов), двойник не может хранить π / 2 с бесконечной точностью, поэтому ваш вход-это что-то очень близкое, но не совсем, 90 градусов - и таким образом, результат-это что-то очень большое, но не совсем бесконечность. Вы можете спросить: "почему они просто не возвращаются Double.POSITIVE_INFINITY , Когда вы проходите через ближайший двойник к π / 2", но это может привести к двусмысленности: что, если вы действительно хотите получить загар этого числа, а не 90 градусов? Или, что если (из-за предыдущей ошибки с плавающей запятой) у вас было что-то, что было немного дальше от π/2, чем самое близкое возможное значение, но для ваших нужд это все еще π/2? Вместо того, чтобы принимать за вас произвольные решения, JDK обрабатывает ваше близкое к-но-не-точно число π / 2 на лице значение, и таким образом дает вам большой, но не бесконечный результат.

Для некоторых операций, особенно связанных с деньгами, вы можете использовать BigDecimal для устранения ошибок с плавающей запятой: вы можете действительно представлять значения, такие как 0.1 (вместо значения, действительно очень близкого к 0.1, что является лучшим, что может сделать float или double). Но это намного медленнее и не помогает вам в таких вещах, как sin/cos (по крайней мере, со встроенными библиотеками).

* это, вероятно, не совсем Дзен, но в разговорном языке смысл

Вы должны использовать BigDecimal вместо double. К сожалению, StrictMath не поддерживает BigDecimal, поэтому вам придется использовать другую библиотеку или собственную реализацию sin/cos/tan.

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

Существует несколько решений. Один из них-использовать математический пакет расширенной точности - BigDecimal часто предлагается для Java. BigDecimal может обрабатывать намного больше цифр точности, а также - поскольку это десятичное представление, а не 2-е-дополняющее представление-имеет тенденцию округляться способами, которые менее удивительны для людей, которые привыкли работать в базе 10. (Это не обязательно делает их более правильными, Обратите внимание. Двоичный не может точно представлять 1/3,но и десятичный не может.)

Существуют также представления с плавающей запятой, дополняющие представления с расширенной точностью 2. Java напрямую поддерживает float и double (которые обычно также поддерживаются аппаратным обеспечением), но можно написать версии, которые поддерживают больше цифр точности.

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

Другой может использовать двоичный код с фиксированной точкой, а не с плавающей точкой. Например, стандартным решением для большинства финансовых расчетов является простое вычисление в терминах наименьшей единицы валюты-Пенни, в США-в целых числах, преобразование в формат отображения и из него (например, доллары и центы) только для ввода-вывода. количество миллисекунд (или наносекунд, если вы используете вызов nanotime), что дает более чем достаточную точность и более чем достаточный диапазон значений для большинства практических целей. Опять же, это означает, что округление имеет тенденцию происходить таким образом, который соответствует человеческим ожиданиям... и опять же, это не столько о точности, сколько о том, чтобы не удивлять пользователей. И эти представления, поскольку они обрабатываются как целые числа или лонги, позволяют быстро вычислять - быстрее, чем с плавающей точкой, в факт.

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