Сравнение двойных значений на равенство в Java.

Я хотел бы получить несколько советов от людей, которые имеют больший опыт работы с примитивным double равенством в Java. Использование d1 == d2 для двух двойных d1 и d2 недостаточно из-за возможных ошибок округления.

Мои вопросы:

  1. Является ли Java Double.compare(d1,d2) == 0 обработкой ошибок округления в какой-то степени? Как описано в документации 1.7 он возвращает значение 0, если d1 численно равно d2. Кто-нибудь уверен, что именно они подразумевают под числовым равенством?

  2. Используя расчет относительной ошибки относительно некоторого значения дельты, есть ли общее (не зависящее от приложения) значение дельты, которое вы бы порекомендовали? См. Пример ниже.

Ниже приведена общая функция для проверки равенства с учетом относительной ошибки. Какое значение delta вы бы порекомендовали для выявления большинства ошибок округления из простых операций +, -, /, * операции?

public static boolean isEqual(double d1, double d2) {
    return d1 == d2 || isRelativelyEqual(d1,d2);
}

private static boolean isRelativelyEqual(double d1, double d2) {
    return delta > Math.abs(d1- d2) / Math.max(Math.abs(d1), Math.abs(d2));
}

person chr0mzie    schedule 06.08.2014    source источник
comment
возможный дубликат Как решить проблему двойного округления Java   -  person om-nom-nom    schedule 06.08.2014
comment
возможный дубликат Почему Java Double.compare ( double, double) реализовано как есть?   -  person avalancha    schedule 06.08.2014
comment
Ваш isRelativelyEqual - правильный подход (помимо проблем с NaN) для тотальной атаки на проблему. Вы бы выбрали delta на основе количества бит точности в вашем формате с плавающей запятой, фактически 53 бит или 16 десятичных цифр для двойной точности, и насколько близко вы хотите быть. В качестве альтернативы вы можете использовать doubleToLongBits для чисел, округлить дробь, чтобы исключить 1–3 дробных бита, а затем сравнить на равные.   -  person Hot Licks    schedule 06.08.2014


Ответы (3)


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

Один особенно плохой случай - если вы вычтите два почти равных числа, например 1.0000000001 - 1.0, и сравните результат с 0.0000000001.

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

Например, вывод

public class Main {

    public static double delta(double d1, double d2) {
        return Math.abs(d1- d2) / Math.max(Math.abs(d1), Math.abs(d2));
    }

    public static void main(String[] args) {
        System.out.println(delta(0.1*0.1, 0.01));
        System.out.println(delta(1.0000000001 - 1.0, 0.0000000001));
    }

}

is

1.7347234759768068E-16
8.274036411668976E-8

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

person Henry    schedule 06.08.2014
comment
Что, если ожидаемое значение равно 0,0? - person simon.watts; 04.10.2017
comment
@ simon.watts Ничего особенного, как я сказал выше: мало надежды найти общий метод, который был бы применим во всех ситуациях. - person Henry; 04.10.2017

Вы можете попробовать что-то вроде этого (не проверено):

public static int sortaClose(double d1, double d2, int bits) {
    long bitMask = 0xFFFFFFFFFFFFFFFFL << bits;
    long thisBits = Double.doubleToLongBits(d1) & bitMask;
    long anotherBits = Double.doubleToLongBits(d2) & bitMask;

    if (thisBits < anotherBits) return -1;
    if (thisBits > anotherBits) return 1;
    return 0;                        
}

«биты» обычно составляют от 1 до 4 или около того, в зависимости от того, насколько точно вы хотите отсечение.

Уточнение может заключаться в добавлении 1 к позиции первого обнуляемого бита перед маскированием (для «округления»), но тогда вам придется беспокоиться о пульсации вплоть до самого старшего бита.

person Hot Licks    schedule 06.08.2014
comment
Спасибо, это тот подход, который я искал. Однако это не совсем работает с 0 - положительные + ε и отрицательные -ε не считаются близкими. Вот альтернативная версия, которая прошла тесты, которые я провел (но я бы не был шокирован, если где-то есть какой-то сбой): return d1==d2 /* hotpath; also handles infinities and NaNs. */ || (Math.abs(d1-d2) < Math.max( Math.ulp(d1), Math.ulp(d2) ) * (0b1L << bits)); - person not-just-yeti; 18.09.2019

Из javadoc для compareTo

  • Этот метод считает, что Double.NaN равно самому себе и больше всех других значений типа double (включая Double.POSITIVE_INFINITY).
  • 0,0d считается этим методом больше, чем -0,0d.

Вы можете найти эту статью очень полезно

Если хочешь, можешь проверить лайк

double epsilon = 0.0000001;
if      ( d <= ( 0 - epsilon ) ) { .. }
else if ( d >= ( 0 + epsilon ) ) { .. }
else { /* d "equals" zero */ }
person Rahul Tripathi    schedule 06.08.2014