Почему BigDecimal.equals заданы для сравнения как значения, так и масштаба отдельно?

Это не вопрос о том, как сравнивать два объекта BigDecimal - я знаю, что для этого можно использовать compareTo вместо equals, так как equals документируется как:

В отличие от compareTo, этот метод рассматривает два объекта BigDecimal равными, только если они равны по значению и масштабу (таким образом, 2.0 не сравнивается с 2.00 при сравнении с этим методом).

Возникает вопрос: почему этот equals был указан в этом, казалось бы, интуитивно понятном? То есть, почему важно иметь возможность различать от 2.0 до 2.00?

Кажется вероятным, что для этого должна быть причина, так как документация Comparable, которая указывает метод compareTo, гласит:

Настоятельно рекомендуется (хотя и не обязательно), чтобы естественные порядки соответствовали равенствам

Я полагаю, что есть хорошая причина для игнорирования этой рекомендации.

Ответ 1

Поскольку в некоторых ситуациях может быть важна индикация точности (то есть границы погрешности).

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

Ответ 2

Точка, которая еще не рассмотрена ни в одном из других ответов, заключается в том, что equals требуется согласовать с hashCode и стоимостью реализации hashCode, которая должна была дать такое же значение для 123.0 по состоянию на 123.00 (но все же сделать разумную работу по различению разных значений) было бы намного больше, чем реализация hashCode, которая не требовалась для этого. В настоящей семантике hashCode требуется умножить на 31 и добавить для каждого 32 бита сохраненного значения. Если бы hashCode требовалось быть согласованным между значениями с различной точностью, ему приходилось либо вычислять нормированную форму любого значения (дорого), либо, как минимум, делать что-то вроде вычисления цифрового корня base-999999999 значения и умножьте это, mod 999999999, основанный на точности. Внутренняя петля такого метода будет:

temp = (temp + (mag[i] & LONG_MASK) * scale_factor[i]) % 999999999;

замена множителя на 31 с помощью операции с 64-битным модулем - намного дороже. Если требуется хэш-таблица, которая считает эквивалентно числовые значения BigDecimal эквивалентными, и большинство ключей, которые ищутся в таблице, будут найдены, эффективным способом достижения желаемого результата будет использование хеш-таблицы, в которой хранятся обертки значений, а не непосредственно хранить значения. Чтобы найти значение в таблице, начните с поиска самого значения. Если ни один не найден, нормализуйте значение и найдите его. Если ничего не найдено, создайте пустую оболочку и сохраните запись в оригинальной и нормализованной форме номера.

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

Ответ 3

В математике 10,0 равно 10,00. В физике 10.0м и 10.00м, возможно, различны (разная точность), когда я говорю об объектах в ООП, я бы определенно сказал, что они не равны.

Также легко представить себе неожиданную функциональность, если равные игнорируют масштаб (например, если a.equals(b), разве вы не ожидаете a.add(0.1).equals(b.add(0.1)?).

Ответ 4

Если числа округляются, это показывает точность вычисления - другими словами:

  • 10.0 может означать, что точное число было между 9,95 и 10,05
  • 10.00 может означать, что точное число было между 9.995 и 10.005

Другими словами, он связан с арифметической точностью.

Ответ 5

Я полагаю, что есть хорошая причина для игнорирования этой рекомендации.

Может и нет. Я предлагаю простое объяснение, что дизайнеры BigDecimal просто сделали плохой выбор дизайна.

  • Хороший дизайн оптимизирован для общего использования. Большую часть времени ( > 95%) люди хотят сравнить две величины на основе математического равенства. Для меньшинства того времени, когда вы действительно заботитесь о том, чтобы оба числа были одинаковыми как по шкале, так и по стоимости, для этой цели мог бы быть дополнительный метод.
  • Это идет вразрез с ожиданиями людей и создает ловушку, в которую очень легко попасть. Хороший API подчиняется "принципу наименьшего удивления".
  • Это нарушает обычное соглашение Java, которое Comparable соответствует равенству.

Интересно, что класс Scala BigDecimal (который реализован с использованием Java BigDecimal под капотом) сделал противоположный выбор:

BigDecimal("2.0") == BigDecimal("2.00")     // true

Ответ 6

Метод compareTo знает, что конечные нули не влияют на числовое значение, представленное BigDecimal, что является единственным аспектом compareTo. Напротив, метод equals обычно не имеет способа узнать, какие аспекты объекта кому-то нужны, и поэтому должен возвращать true, если два объекта эквивалентны во всех отношениях, которые может интересовать программист. Если x.equals(y) верно, для x.toString().equals(y.toString()) было бы довольно неожиданно выводить false.

Еще одна проблема, которая, возможно, даже более значима, заключается в том, что BigDecimal по существу сочетает в себе BigInteger и масштабный коэффициент, так что если два числа представляют одно и то же значение, но имеют разные числа конечных нулей, то будет существовать a BigInteger значение которого имеет некоторую степень в десять раз другую. Если для равенства требуется, чтобы мантисса и масштаб совпадали, то hashCode() для BigDecimal может использовать хэш-код BigInteger. Если возможно, что два значения считаются "равными", даже если они содержат разные значения BigInteger, это значительно усложнит ситуацию. Тип BigDecimal, который использовал свое собственное хранилище для хранения, а не BigInteger, мог быть реализован различными способами, чтобы позволить номерам быстро хэшировать таким образом, чтобы значения, представляющие одинаковое число, сравнивались бы равными (как простой пример, версия, которая упаковала девять десятичных цифр в каждом значении long и всегда требовала, чтобы десятичная точка находилась между группами из девяти, могла вычислить хеш-код таким образом, чтобы игнорировать конечные группы, значение которых было равно нулю), но a BigDecimal, который инкапсулирует a BigInteger, не может этого сделать.