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

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

В отличие от compareTo, этот метод считает два объекта BigDecimal равными, только если они равны по значению и масштабу (таким образом, 2,0 не равно 2,00 при сравнении с помощью этого метода).

Возникает вопрос: почему equals был указан таким, казалось бы, нелогичным образом? То есть, почему важно различать 2,0 и 2,00?

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

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

Я полагаю, что должна быть веская причина игнорировать эту рекомендацию.


person bacar    schedule 31.12.2012    source источник
comment
Ничего не стоит new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0   -  person Peter Lawrey    schedule 31.12.2012


Ответы (7)


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

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

person Oliver Charlesworth    schedule 31.12.2012
comment
Думаю, я не думал об использовании BigDecimal для определения степени точности (точно так же, как тип, который допускает произвольные количества точности). С этой точки зрения это имеет смысл, однако тогда я должен отказаться от мысли об объекте как о числовом типе - он не ведет себя так же, как и в отношении equals. - person bacar; 31.12.2012
comment
По моему опыту, ситуации, в которых вы хотите equals() зафиксировать эту семантическую разницу в точности, встречаются гораздо реже, чем интуитивно понятный случай. Вдобавок к этому, интуитивно понятный случай будет означать, что compareTo() BigDecimal будет соответствовать equals(). На мой взгляд, солнце здесь ошиблось. - person bowmore; 31.12.2012
comment
@bowmore, это тоже мое предположение, но опыт разный. Пуристы могут возразить, что им следовало предоставить 2 класса - один класс, не подходящий для сортировки (не compareTo), который фиксирует точность как видимую часть объекта; и второй класс, реализующий Comparable с compareTo в соответствии с equals, который обрабатывает масштаб и значение в целом. Однако обеспечение того и другого могло показаться довольно раздутым / непрагматичным и создавать, а не устранять путаницу - Sun разрешила обе функции, предоставив несовместимые compareTo и equals (что удивило многих из нас на этом пути). - person bacar; 31.12.2012
comment
@bacar реализация с таким методом, как say boolean equalsWithPrecision(BigDecimal other), позволила бы согласовывать обе функции, и. - person bowmore; 31.12.2012
comment
Также кажется, что прервать использование Set и Map. - person Geoffrey De Smet; 20.11.2013
comment
@GeoffreyDeSmet: нарушается ли такое использование, зависит от предполагаемого назначения набора. Если кто-то создает набор для того, чтобы ссылки на эквивалентные, но отличные от них экземпляры могли быть заменены ссылками на единственный экземпляр, поведение equals будет идеальным; Я считаю определения equals, несовместимые с использованием, несколько опасными. - person supercat; 27.07.2014
comment
Я согласен с этой идеей, но IMHO класс под названием Measure с двумя числами: измеренное значение и шкала ошибок было бы лучше, потому что в большинстве случаев ваша инструментальная ошибка не обязательно равна 1 на какой-то цифре. - person user1708042; 06.11.2019

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

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

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

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

person supercat    schedule 25.07.2014
comment
Интересное наблюдение. Корректность важнее производительности, поэтому вы должны иметь краткий список того, что вы считаете правильным поведением класса BigDecimal (т. Е. Следует ли учитывать масштаб / точность для равенства), прежде чем вы начнете рассматривать производительность. Мы понятия не имеем, повлиял ли этот конкретный аргумент на это. Ваши аргументы, конечно же, применимы и к equals. - person bacar; 27.07.2014
comment
@bacar: Есть два вопроса, связанных с эквивалентностью, которые можно разумно задать любому объекту (IHMO, виртуальные методы Object должны были обеспечивать оба): пусть X и Y безопасно считаются эквивалентными, даже если ссылки свободно делятся с внешнего кода, и могут ли X и Y безопасно рассматриваться их владельцем как эквивалентные, если он сохраняет исключительный контроль над X, Y и всеми составляющими изменяемыми состояниями? Я бы предположил, что единственными типами, которые должны определять equals способом, не совпадающим ни с одним из вышеперечисленных, будут те, чьи экземпляры не ожидаются ... - person supercat; 27.07.2014
comment
... подвержен влиянию внешнего мира. Например, если нужно использовать хешированный набор строк, которые сравниваются без учета регистра, можно определить тип CaseInsensitiveStringWrapper, equals и hashCode которого работают с версиями обернутой строки в верхнем регистре. Хотя оболочка будет иметь необычное значение для equals, она не будет открыта для внешнего кода. Поскольку BigDecimal предназначен для использования внешним кодом, он должен сообщать об экземплярах как равные только в том случае, если весь разумный внешний код будет считать их эквивалентными. - person supercat; 27.07.2014
comment
@bacar: Лично я считаю, что ситуация с equals и compareTo методами BigDecimal великолепна: код, который хочет, чтобы вещи сравнивались на основе значения, может использовать compareTo, а код, который хочет сравнивать на основе эквивалентности, может использовать equals. Обратите внимание, что точность влияет не только на результат; Я считаю, что по крайней мере один способ выполнения деления использует точность делимого для контроля точности округления результата, так что 10,0 / 3 будет 3,3, а 10,000 / 3 даст 3,333. Таким образом, замена 10.0 на 10.000 небезопасна. - person supercat; 27.07.2014
comment
Разделение могло быть указано, чтобы вести себя по-другому, если бы равенство было указано иначе. Я думаю, что ваш CaseInsensitiveStringWrapper поднимает очень интересный момент - легко реализовать «нечеткую» эквивалентность поверх более строгой, в то время как может быть сложнее, невозможно или просто удивительно реализовать строгую эквивалентность с точки зрения более нечеткой. В любом случае принцип наименьшего удивления, если он нарушается для той или иной группы пользователей. - person bacar; 28.07.2014
comment
@bacar: Я бы посоветовал, если пользователей учат, что они должны ожидать использовать методы, отличные от equals, когда они хотят проверить отсутствие равенства, тогда никого не нужно удивлять. - person supercat; 28.07.2014

Общее правило для equals состоит в том, что два равных значения должны быть заменены друг на друга. То есть, если выполнение вычисления с использованием одного значения дает некоторый результат, подстановка значения equals в то же вычисление должна дать результат, который equals является первым результатом. Это относится к объектам, которые являются значениями, например String, Integer, BigDecimal и т. Д.

Теперь рассмотрим BigDecimal значения 2,0 и 2,00. Мы знаем, что они численно равны, и что compareTo на них возвращает 0. Но equals возвращает false. Почему?

Вот пример, когда они не подлежат замене:

var a = new BigDecimal("2.0");
var b = new BigDecimal("2.00");
var three = new BigDecimal(3);

a.divide(three, RoundingMode.HALF_UP)
==> 0.7

b.divide(three, RoundingMode.HALF_UP)
==> 0.67

Результаты явно неравны, поэтому значение a не может быть заменено на b. Следовательно, a.equals(b) должно быть ложным.

person Stuart Marks    schedule 12.02.2021
comment
вы заставляете это звучать ооочень легко с помощью этого примера. классно! - person Eugene; 09.03.2021
comment
@Eugene Пример был настолько хорош, что мы решили поместить его в javadoc: github.com/openjdk / jdk / commit / a1181852 (он должен появиться в JDK 17 build 13). - person Stuart Marks; 11.03.2021
comment
… И это приводит к выводу, что мы должны быть осторожны при смешивании порядка и равенства, иначе мы получим ошибки, подобные поведению Stream.of("0.1", "0.10", "0.1") .map(BigDecimal::new) .sorted().distinct() .forEach(System.out::println); - person Holger; 22.03.2021
comment
@Holger Правильно. JDK-8223933. - person Stuart Marks; 23.03.2021

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

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

person Aleksander Blomskøld    schedule 31.12.2012
comment
Да, я ожидал этого, но я не понимаю вашей точки зрения; Я не предлагаю игнорировать масштаб; Я предлагаю рассматривать значение и масштаб как целое, как это делает compareTo. - person bacar; 31.12.2012
comment
В ПОРЯДКЕ. Я понимаю, что иногда пользователи могут захотеть подумать о точности, но я все еще не понимаю, что вы думаете о неожиданной функциональности. Если бы они решили, что 2,0 равняется 2,00, я не уверен, где ваш пример добавления 0,1 вызывает проблемы. - person bacar; 31.12.2012

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

  • 10,0 может означать, что точное число находится в диапазоне от 9,95 до 10,05.
  • 10,00 может означать, что точное число находится в диапазоне от 9,995 до 10,005.

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

person assylias    schedule 31.12.2012

Я полагаю, что должна быть веская причина игнорировать эту рекомендацию.

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

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

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

BigDecimal("2.0") == BigDecimal("2.00")     // true
person Matt R    schedule 11.07.2014
comment
Фундаментальным требованием equals является то, что два объекта с неравными хэш-кодами должны сравниваться неравными, а конструкция BigDecimal такова, что числа с разной точностью хранятся по-разному. Таким образом, если equals будет рассматривать значения с разной точностью как эквивалентные, это сильно ухудшит производительность хэш-таблиц, даже тех, в которых все значения хранились с эквивалентной точностью. - person supercat; 26.07.2014
comment
@supercat Хорошее наблюдение. Однако я бы сказал, что BigDecimal-keyed Maps (и Sets) настолько редки, что это недостаточное оправдание для чувствительного к масштабу equals. - person Matt R; 26.07.2014
comment
Использование таких типов, как ключи карты, может быть не очень распространенным явлением, но, вероятно, также не так уж и редко. Среди прочего, код, который в конечном итоге часто вычисляет похожие значения, иногда может получить огромную выгоду от кеширования часто вычисляемых значений. Чтобы это работало эффективно, необходимо, чтобы хеш-функция была хорошей и быстрой. - person supercat; 27.07.2014
comment
@supercat 1) Можно с уверенностью сказать, что BigDecimal ключи встречаются гораздо реже, чем люди, которых укусило его неинтуитивное определение равенства; 2) если нечувствительный к масштабу хеш является узким местом производительности, вы, вероятно, находитесь в ситуации, когда использование самого BigDecimal слишком медленно (например, вы можете переключиться на longs для денежных расчетов). - person Matt R; 27.07.2014

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

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

person supercat    schedule 21.01.2013
comment
метод equals обычно не имеет возможности узнать, какие аспекты объекта кого-то волнуют - я категорически не согласен с этим утверждением. Классы определяют (иногда неявно) контракт на их внешне видимое поведение, которое включает equals. Классы часто существуют специально для того, чтобы скрыть (путем инкапсуляции) детали, которые не интересуют пользователей. - person bacar; 22.01.2013
comment
Также - я не думаю, что в целом вы должны ожидать, что equals будет соответствовать toString. Классы вольны определять toString в значительной степени так, как они считают нужным. Рассмотрим пример из JDK, Set<String> s1 = new LinkedHashSet<String>(); s1.add("foo"); s1.add("bar"); Set<String> s2 = new LinkedHashSet<String>(); s2.add("bar"); s2.add("foo"); s1 и s2 имеют разные строковые представления, но сравниваются одинаково. - person bacar; 22.01.2013
comment
@bacar: Возможно, я переношу принципы .Net на Java. Хешированные коллекции в .Net позволяют указать методы для сравнения и хеширования на равенство, тем самым эффективно сообщая коллекции, какие аспекты объекта должны ее интересовать. Если бы у кого-то был тип коллекции, который поддерживал свои элементы последовательно, но предлагал SequenceEquals GetSequenceHashCode , ContentEquals и GetContentHashCode, можно затем сохранить такой тип в хэшированной коллекции, используя равенство ссылок, равенство последовательностей или равенство содержимого, не зависящее от порядка. - person supercat; 22.01.2013
comment
Я тоже не согласен с этим утверждением. На собственном опыте я обнаружил, что при переопределении метода equals() в настраиваемых объектах лучше определять эквивалентность в малом масштабе (то есть как можно меньшем количестве атрибутов объекта), чем в большом масштабе. Чем меньше атрибутов способствует эквивалентности, тем лучше. Базы данных работают по тому же принципу. - person ryvantage; 05.01.2014
comment
@ryvantage: обычно не ожидается использовать объекты со многими полями в качестве ключей словаря для поиска другой информации, но особенно при работе с иерархическими коллекциями может быть ряд обстоятельств, когда в конечном итоге получается много копий одного и того же Информация; если можно эффективно идентифицировать ссылки на различные, но эквивалентные объекты, замена ссылок на все, кроме самой старой копии, ссылками на самую старую копию может сэкономить память и улучшить производительность; для этого нужно сравнить все поля. - person supercat; 05.01.2014
comment
Что ж, для меня я использую объекты в своих приложениях, которые моделируются точно так же, как они находятся в базе данных, используя HashSet для хранения большого количества из них, и используя такие методы, как add() и contains(), он ищет эквивалентность, поэтому сначала, когда Я переопределил equals(), он сравнил каждое поле объекта, но если по какой-то причине был добавлен новый элемент, который немного отличался, HashSet сохранил бы их оба, что не было bueno. В итоге я определил равенство (и хеш-значение) исключительно на основе id (первичного ключа) из базы данных. - person ryvantage; 05.01.2014
comment
Итак, в моем понимании, если два объекта имеют одинаковый id, они представляют один и тот же экземпляр объекта, даже если их поля не равны. Это был единственный способ заставить их вести себя так, как Set я использовал. - person ryvantage; 05.01.2014
comment
Являются ли объекты изменяемыми или неизменяемыми и каково их отношение к любому постоянному хранилищу? Если объекты привязаны к строкам в базе данных, я бы предположил, что несколько разных объектов, прикрепленных к одной строке, не должны существовать в первую очередь. В противном случае мне не совсем понятно, почему вы используете Set, а не Map? Я бы подумал, что естественным способом хранения вещей была бы карта, ключевой объект которой инкапсулирует те части данных, которые имеют отношение к равенству. - person supercat; 05.01.2014