Перейти к основному содержимому

Сравнение двойников в Java

· 4 мин. чтения

1. Обзор

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

Во-первых, мы объясним, почему использование простого оператора == является неточным и может привести к трудно отслеживаемым ошибкам во время выполнения. Затем мы покажем, как правильно сравнивать двойники в простой Java и в распространенных сторонних библиотеках.

2. Использование оператора ==

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

Из-за неточности округления могут возникнуть интересные ошибки:

double d1 = 0;
for (int i = 1; i <= 8; i++) {
d1 += 0.1;
}

double d2 = 0.1 * 8;

System.out.println(d1);
System.out.println(d2);

Обе переменные, d1 и d2, должны быть равны 0,8. Однако, когда мы запустим приведенный выше код, мы увидим следующие результаты:

0.7999999999999999
0.8

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

Если мы хотим иметь наилучшую точность и контроль над механизмом округления, мы можем использовать класс java.math.BigDecimal .

3. Сравнение двойников в простой Java

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

double epsilon = 0.000001d;

assertThat(Math.abs(d1 - d2) < epsilon).isTrue();

Чем меньше значение эпсилон, тем выше точность сравнения. Однако, если мы укажем слишком маленькое значение допуска, мы получим тот же ложный результат, что и при простом сравнении ==. В общем, значение эпсилон с 5 и 6 десятичными знаками обычно является хорошим местом для начала .

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

4. Использование математики Apache Commons

Apache Commons Math — одна из крупнейших библиотек с открытым исходным кодом, посвященная компонентам математики и статистики. Из множества различных классов и методов мы сосредоточимся , в частности, на классе org.apache.commons.math3.util.Precision . Он содержит 2 полезных метода equals() для правильного сравнения двойных значений :

double epsilon = 0.000001d;

assertThat(Precision.equals(d1, d2, epsilon)).isTrue();
assertThat(Precision.equals(d1, d2)).isTrue();

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

Версия функции с двумя аргументами — это просто сокращение для вызова метода equals(d1, d2, 1) . В этом случае d1 и d2 считаются равными, если между ними нет чисел с плавающей запятой. ``

5. Использование гуавы

Google Guava — это большой набор основных библиотек Java, которые расширяют стандартные возможности JDK. Он содержит большое количество полезных математических утилит в пакете com.google.common.math . Чтобы правильно сравнивать двойные значения в Guava, давайте реализуем метод fuzzyEquals() из класса DoubleMath :

double epsilon = 0.000001d;

assertThat(DoubleMath.fuzzyEquals(d1, d2, epsilon)).isTrue();

Имя метода отличается от имени метода Apache Commons Math, но внутри он работает практически так же. Единственное отличие состоит в том, что нет перегруженного метода со значением эпсилон по умолчанию.

6. Использование JUnit

JUnit — одна из наиболее широко используемых сред модульного тестирования для Java. В общем, каждый модульный тест обычно заканчивается анализом разницы между ожидаемыми и фактическими значениями. Поэтому фреймворк для тестирования должен иметь корректные и точные алгоритмы сравнения. Фактически, JUnit предоставляет набор методов сравнения для общих объектов, коллекций и примитивных типов, включая специальные методы для проверки равенства двойных значений:

double epsilon = 0.000001d;
assertEquals(d1, d2, epsilon);

На самом деле, он работает так же, как ранее описанные методы Guava и Apache Commons.

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

7. Заключение

В этой статье мы рассмотрели различные способы сравнения двойных значений в Java.

Мы объяснили, почему простое сравнение может привести к трудно отслеживаемым ошибкам во время выполнения. Затем мы показали, как правильно сравнивать значения в простой Java и обычных библиотеках.

Как всегда, исходный код примеров можно найти на GitHub .