Почему BigDecimal естественный порядок несовместим с равными?
Из Javadoc для BigDecimal
:
Примечание: следует соблюдать осторожность, если BigDecimal
объекты используются как ключи в SortedMap
или в элементах в SortedSet
, так как BigDecimal
естественное упорядочение несовместимо с равными.
Например, если вы создадите HashSet
и добавьте new BigDecimal("1.0")
и new BigDecimal("1.00")
к нему, набор будет содержать два элемента (поскольку значения имеют разные шкалы, поэтому они не равны в соответствии с equals
и hashCode
), но если вы сделаете то же самое с TreeSet
, набор будет содержать только один элемент, потому что значения сравниваются как равные при использовании compareTo
.
Есть ли какая-то конкретная причина этой несогласованности?
Ответы
Ответ 1
Из реализация OpenJDK в BigDecimal:
/**
* Compares this {@code BigDecimal} with the specified
* {@code Object} for equality. Unlike {@link
* #compareTo(BigDecimal) compareTo}, this method considers two
* {@code BigDecimal} objects equal only if they are equal in
* value and scale (thus 2.0 is not equal to 2.00 when compared by
* this method).
*
* @param x {@code Object} to which this {@code BigDecimal} is
* to be compared.
* @return {@code true} if and only if the specified {@code Object} is a
* {@code BigDecimal} whose value and scale are equal to this
* {@code BigDecimal}'s.
* @see #compareTo(java.math.BigDecimal)
* @see #hashCode
*/
@Override
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflate().equals(xDec.inflate());
}
Больше от реализации:
* <p>Since the same numerical value can have different
* representations (with different scales), the rules of arithmetic
* and rounding must specify both the numerical result and the scale
* used in the result representation.
Вот почему реализация equals
учитывает scale
. Конструктор, который берет строку как параметр, реализуется следующим образом:
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
где третий параметр будет использоваться для scale
(в другом конструкторе), поэтому строки 1.0
и 1.00
будут создавать разные BigDecimals (с разными шкалами).
Из Эффективная Java Джошуа Блох:
Последний абзац договора compareTo, который является сильным скорее, чем истинное положение, просто заявляет, что тест равенства, наложенный методом compareTo, должен, как правило, возвращаться те же результаты, что и метод equals. Если это положение соблюдается, порядок, наложенный методом compareTo, называется согласованным с равными. Если это нарушено, заказ считается непоследовательным с равными. Класс, метод compareTo которого налагает порядок, который несовместимые с равными, по-прежнему будут работать, но отсортированные коллекции содержащие элементы класса, могут не подчиняться общему договору соответствующие интерфейсы коллекции (Collection, Set или Map). Эта потому что общие контракты для этих интерфейсов определены в условия метода equals, но отсортированные коллекции используют равенство тест, налагаемый compareTo вместо равных. Это не катастрофа если это произойдет, но это то, о чем нужно знать.
Ответ 2
Поведение кажется разумным в контексте арифметической точности, где конечные нули значимые цифры и 1.0 не имеют того же значения, что и 1.00. Сделать их неравными, по-видимому, разумным выбором.
Однако с точки зрения сравнения ни одна из двух больше или меньше, чем другая, а интерфейс Comparable требует полного порядка (т.е. каждый BigDecimal должен быть сопоставим с любым другим BigDecimal). Единственным разумным вариантом здесь было определение полного порядка, так что метод compareTo рассмотрел бы два числа.
Обратите внимание, что несогласованность между равными и compareTo не является проблемой, если она задокументирована. Это даже иногда то, что вам нужно.
Ответ 3
BigDecimal работает, имея два числа, целое число и масштаб. Целое число - это "число", а масштаб - это число цифр справа от десятичной точки. В основном базовое число с плавающей запятой.
Когда вы говорите "1.0"
и "1.00"
, это технически разные значения в формате BigDecimal:
1.0
integer: 10
scale: 1
precision: 2
= 10 x 10 ^ -1
1.00
integer: 100
scale: 2
precision: 3
= 100 x 10 ^ -2
В научной нотации вы не сделали бы ни того, ни другого, это должно быть 1 x 10 ^ 0
или просто 1
, но BigDecimal позволяет это.
В compareTo
масштаб игнорируется и оценивается как обычные числа 1 == 1
. В equals
сравниваются значения целых чисел и шкалы, 10 != 100
и 1 != 2
. Метод BigDecimal equals игнорирует проверку object == this
, я предполагаю, потому что намерение состоит в том, что каждый BigDecimal рассматривается как тип числа, а не как объект.
Я бы упомянул об этом:
// same number, different types
float floatOne = 1.0f;
double doubleOne = 1.0;
// true: 1 == 1
System.out.println( (double)floatOne == doubleOne );
// also compare a float to a double
Float boxFloat = floatOne;
Double boxDouble = doubleOne;
// false: one is 32-bit and the other is 64-bit
System.out.println( boxInt.equals(boxDouble) );
// BigDecimal should behave essentially the same way
BigDecimal bdOne1 = new BigDecimal("1.0");
BigDecimal bdOne2 = new BigDecimal("1.00");
// true: 1 == 1
System.out.println( bdOne1.compareTo(bdOne2) );
// false: 10 != 100 and 1 != 2 ensuring 2 digits != 3 digits
System.out.println( bdOne1.equals(bdOne2) );
Поскольку BigDecimal допускает определенную "точность", сравнение как целого, так и шкалы более или менее такое же, как сравнение как числа, так и точности.
Несмотря на то, что это говорит о методе BigDecimal precision(), который всегда возвращает 1, если BigDecimal равен 0. В этом случае compareTo && & точность оценивает значение true и равно принимает значение false. Но 0 * 10 ^ -1
не должен равняться 0 * 10 ^ -2
, потому что первый - это 2-значное число 0.0
, а последнее - 3-значное число 0.00
. Метод equals сравнивает как значение, так и количество цифр.
Я полагаю, это странно, что BigDecimal допускает завершение нулей, но это в основном необходимо. Для выполнения математической операции типа "1.1" + "1.01"
требуется преобразование, но "1.10" + "1.01"
нет.
Итак, compareTo
сравнивает BigDecimals как числа и equals
сравнивает BigDecimals как BigDecimals.
Если сравнение нежелательно, используйте список или массив, где это не имеет значения. HashSet и TreeSet, конечно, предназначены специально для хранения уникальных элементов.
Ответ 4
Ответ довольно короткий. Метод equals() сравнивает объекты, а compareTo() сравнивает значения. В случае BigDecimal различные объекты могут представлять одно и то же значение. То почему equals() может возвращать false, в то время как compareTo() возвращает 0.
равные объекты = > равные значения
равные значения =/" > равные объекты
Объект - это просто компьютерное представление некоторого реального значения. Например, одно изображение может быть представлено в форматах GIF и JPEG. Это очень похоже на BigDecimal, где одинаковое значение может иметь различные представления.