«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 Math

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();

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

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

5. Использование Guava

Guava от Google — это большой набор основных библиотек 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.