Может ли экземпляр быть равен некоторому другому экземпляру более определенного типа?

25

Я читал эту статью: Как написать метод равенства в Java .

По сути, он предоставляет решение для метода equals (), который поддерживает наследование:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

Но хорошая ли это идея? эти два экземпляра кажутся равными, но могут иметь два разных хеш-кода. Разве это не так?

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

Wes
источник
1
Пример с цветными точками, приведенный в ссылке, имеет больше смысла для меня. Я бы посчитал, что 2D-точка (x, y) может рассматриваться как 3D-точка с нулевой Z-компонентой (x, y, 0), и я хотел бы, чтобы равенство возвращало false в вашем случае. На самом деле, в этой статье четко указано, что ColoredPoint отличается от Point и всегда возвращает false.
coredump
10
Нет ничего хуже, чем учебники, которые нарушают общепринятые правила ... Требуются годы, чтобы избавиться от подобных привычек у программистов.
CorsiKa
3
@coredump Рассмотрение 2D-точки как имеющей нулевую zкоординату может быть полезным соглашением для некоторых приложений (на ум приходят ранние системы CAD, работающие с устаревшими данными). Но это произвольное соглашение. Плоскости в пространствах с 3 и более измерениями могут иметь произвольную ориентацию ... это то, что делает интересные проблемы интересными.
Бен Руджерс
2
Это более чем неправильно .
Кевин Крумвиде

Ответы:

71

Это не должно быть равенством, потому что оно нарушает транзитивность . Рассмотрим эти два выражения:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

Поскольку равенство транзитивно, это должно означать, что следующее выражение также верно:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

Но, конечно, это не так.

Итак, ваша идея приведения правильна - ожидайте, что в Java приведение просто означает приведение типа ссылки. Что вам действительно нужно, так это метод преобразования, который создаст новый Point2Dобъект из Point3Dобъекта. Это также сделало бы выражение более значимым:

twoD.equals(threeD.projectXY())
Идан Арье
источник
1
В статье описываются реализации, которые нарушают транзитивность, и предлагается ряд обходных путей. В области, где мы разрешаем 2D-точки, мы уже решили, что третье измерение не имеет значения. и так (10, 20, 50)равных (10, 20, 60)в порядке. Мы заботимся только о 10и 20.
Бен Руджерс
1
Должен ли Point2Dбыть projectXYZ()метод, чтобы обеспечить Point3Dпредставление себя? Другими словами, должны ли реализации знать друг друга?
hjk
4
@hjk Избавиться от этого Point2Dкажется проще, поскольку проецирование 2D-точек требует сначала определения их плоскости в 3D-пространстве. Если 2D-точка знает, что это плоскость, то это уже 3D-точка. Если это не так, он не может проецироваться. Мне напомнили о равнине Эбботта .
Бен Руджерс
@benrudgers Вы можете, однако, определить Plane3Dобъект, который будет определять плоскость в трехмерном пространстве, у этой плоскости может быть liftметод (2D-> 3D поднимается, а не проецируется), который примет a Point2Dи число для «третьей оси». «- расстояние от плоскости по плоскости нормали. Для простоты использования вы можете определить общие плоскости как статические константы, чтобы вы могли делать такие вещи, какPlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye
@IdanArye Я комментировал предположение, что у 2D точек должен быть метод проекции. Что касается плоскостей с методами подъёма, я думаю, что для этого потребуется два аргумента: точка 2D и плоскость, на которой она предполагается, т.е. она действительно должна быть проекцией, если ей не принадлежит точка ... и если ей принадлежит точка, почему бы просто не владеть 3D-точкой и покончить с проблемным типом данных и запахом метода клудгеда? YMMV.
Бен Руджерс
10

Я ухожу от прочтения статьи, размышляя о мудрости Алана Дж. Перлиса:

Эпиграмма 9. Лучше иметь 100 функций, работающих с одной структурой данных, чем 10 функций с 10 структурами данных.

Тот факт, что правильное «равенство» является той проблемой, которая не дает Мартину Ордски изобретателю Scala работать ночью, должен дать повод задуматься о том, equalsявляется ли переопределение в дереве наследования хорошей идеей.

Что происходит, когда нам не везет, так это ColoredPointто, что наша геометрия дает сбой, потому что мы использовали наследование для распространения типов данных вместо того, чтобы создать один хороший. Это несмотря на необходимость вернуться и изменить корневой узел дерева наследования, чтобы заставить equalsработать. Почему бы просто не добавить zи colorк Point?

На это есть веская причина, Pointи они ColoredPointработают в разных доменах ... по крайней мере, если эти домены никогда не смешиваются. Но если это так, нам не нужно переопределять equals. Сравнение ColoredPointи Pointравенство имеет смысл только в третьей области, где им разрешено смешиваться. И в этом случае, вероятно, лучше иметь «равенство», адаптированное к этому третьему домену, а не пытаться применять семантику равенства из одного или другого или обоих из неотмеченных доменов. Другими словами, «равенство» должно быть определено локально по отношению к месту, где с обеих сторон течет грязь, потому что мы можем не захотеть ColoredPoint.equals(pt)потерпеть неудачу в случаях, Pointдаже если автор ColoredPointсчитает, что это была хорошая идея шесть месяцев назад в 2 часа ночи. ,

Бен Руджерс
источник
6

Когда старые боги программирования изобретали объектно-ориентированное программирование с классами, они решили, когда дело доходит до композиции и наследования, иметь два отношения для объекта: «является» и «имеет».
Это частично решило проблему отличия подклассов от родительских классов, но сделало их пригодными для использования без нарушения кода. Поскольку экземпляр подкласса «является» объектом суперкласса и может быть заменен непосредственно на него, даже если подкласс имеет больше функций-членов или элементов данных, «has a» гарантирует, что он будет выполнять все функции родительского элемента и будет иметь все свои члены. Таким образом, вы можете сказать, что Point3D - это «Point, а Point2D» - это «Point», если они оба наследуются от Point. Кроме того, Point3D может быть подклассом Point2D.

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

Итак, вы получите таблицу сужающих равенств:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

Как правило, вы выбираете самые строгие правила, которые по-прежнему будут выполнять все необходимые функции в вашей проблемной области. Встроенные тесты на равенство для чисел предназначены для того, чтобы быть настолько ограничительными, насколько это возможно для математических целей, но у программиста есть много способов обойти это, если это не является целью, включая округление вверх / вниз, усечение, gt, lt и т. Д. , Объекты с временными метками часто сравниваются по времени их генерации, поэтому каждый экземпляр должен быть уникальным, чтобы сравнения становились очень конкретными.

Фактором проектирования в этом случае является определение эффективных способов сравнения объектов. Иногда вам нужно сделать рекурсивное сравнение всех элементов данных объектов, и это может оказаться очень дорогостоящим, если у вас много и много объектов с множеством элементов данных. Альтернативы состоят в том, чтобы сравнивать только релевантные значения данных или заставить объект генерировать хеш-значение из соответствующих элементов данных для быстрого сравнения с другими подобными объектами, сортировать и сокращать коллекции, чтобы проводить сравнения быстрее и менее интенсивно, и, возможно, разрешать объекты, которые идентичны в данных, которые будут отбракованы, и на его место будет помещен дублирующий указатель на один объект.

Крис Рид
источник
2

Правило гласит: всякий раз, когда вы переопределяете hashcode(), вы переопределяете equals(), и наоборот. Является ли это хорошей идеей или нет, зависит от предполагаемого использования. Лично я бы использовал другой метод ( isLike()или похожий) для достижения того же эффекта.

TMN
источник
1
Может быть нормально переопределить hashCode без переопределения equals. Например, можно было бы сделать это, чтобы проверить другой алгоритм хеширования для того же условия равенства.
Патриция Шанахан
1

Для классов, не являющихся общедоступными, часто бывает полезно иметь метод тестирования эквивалентности, который позволяет объектам разных типов считать друг друга «равными», если они представляют одну и ту же информацию, но потому, что в Java нет средств, с помощью которых классы могут выдавать себя за каждого с другой стороны, часто полезно иметь один открытый тип оболочки в тех случаях, когда возможно иметь эквивалентные объекты с разными представлениями.

Например, рассмотрим класс, инкапсулирующий неизменную двумерную матрицу doubleзначений. Если один внешний метод запрашивает единичную матрицу размером 1000, второй запрашивает диагональную матрицу и передает массив, содержащий 1000 единиц, а третий запрашивает двумерную матрицу и передает массив 1000x1000, где все элементы на первичной диагонали равны 1,0. и все остальные равны нулю, объекты, предоставленные всем трем классам, могут использовать разные внутренние хранилища (первое имеет одно поле для размера, второе имеет массив из тысячи элементов, а третье имеет тысячу массивов из 1000 элементов), но должны сообщать друг о друге как эквивалентные [поскольку все три инкапсулируют неизменную матрицу 1000x1000 с единицами на диагонали и нулями повсюду в другом месте].

Помимо того факта, что он скрывает существование различных типов хранилища резервных копий, оболочка также будет полезна для облегчения сравнений, поскольку проверка элементов на эквивалентность обычно представляет собой многоэтапный процесс. Спросите первый элемент, если он знает, равен ли он второму; если он не знает, спросите второго, знает ли он, равен ли он первому. Если ни один объект не знает, то спросите каждый массив о содержимом его отдельных элементов [можно добавить другие проверки, прежде чем принять решение о длинном-медленном маршруте сравнения отдельных элементов].

Обратите внимание, что метод проверки эквивалентности для каждого объекта в этом сценарии должен возвращать значение из трех состояний («Да, я не эквивалентен», «Нет, я не эквивалентен» или «Я не знаю»), поэтому нормальный метод «равно» не подходит. В то время как любой объект может просто ответить «я не знаю», когда его спрашивают о любой другой, добавление логики, например, к диагональной матрице, которая не будет беспокоить запрос какой-либо единичной матрицы или диагональной матрицы о каких-либо элементах вне главной диагонали, значительно ускорит сравнение среди таких типы.

Supercat
источник