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 .