Чи може екземпляр бути рівним якомусь іншому екземпляру більш конкретного типу?


25

Я читав цю статтю: Як написати метод рівності на Java .

В основному, він пропонує рішення для методу equals (), який підтримує успадкування:

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

Але це гарна ідея? ці два екземпляри здаються рівними, але можуть мати два різних хеш-коди. Хіба це трохи не так?

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


1
Приклад із кольоровими точками, наведеними у посиланні, має для мене більше сенсу. Я вважаю, що 2D-точку (x, y) можна розглядати як 3D-точку з нульовою складовою Z (x, y, 0), і я хотів би, щоб рівність повертала помилкові у вашому випадку. Насправді, у статті про ColoredPoint явно сказано, що вона відрізняється від точки і завжди повертає помилку.
coredump

10
Нічого гіршого, ніж підручники, які порушують загальні умовності ... Потрібні роки, щоб вирвати такі звички у програмістів.
corsiKa

3
@coredump Розгляд 2D точки як нульової zкоординати може бути корисною умовою для деяких застосувань (на розум приходять ранні системи CAD, що обробляють застарілі дані). Але це довільна конвенція. Площини в просторах з 3 і більше розмірами можуть мати довільну орієнтацію ... саме це робить цікаві проблеми цікавими.
ben rudgers

Відповіді:


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.
ben rudgers

1
Чи повинен Point2Dмати projectXYZ()метод надання Point3Dпредставлення про себе? Іншими словами, чи повинні впровадження знати один одного?
hjk

4
@hjk Позбутися Point2Dздається простішим, оскільки проектування 2D точок вимагає спочатку визначити їх площину в тривимірному просторі. Якщо точка 2D знає, що це площина, то це вже точка 3D. Якщо цього немає, він не може проектувати. Я згадую Еббот Флатландія .
ben rudgers

@benrudgers Ви можете, однак, визначити Plane3Dоб’єкт, який визначатиме площину в тривимірному просторі, у цій площині може бути liftметод (2D-> 3D піднімається, а не проектується), який прийме а Point2Dі число для "третьої осі" "- відстань від площини вздовж площини нормальна. Для зручності використання ви можете визначити звичайні площини як статичні константи, щоб ви могли робити такі речі, якPlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye

@IdanArye Я коментував припущення про те, що 2D бали повинні мати метод проекції. Що стосується літаків з методами підйому, я думаю, що це потребує двох аргументів, щоб мати сенс: 2D точка і площина, на яку, як передбачається, знаходиться, тобто вона справді повинна бути проекцією, якщо їй не належить точка ... і якщо йому належить точка, чому б не просто володіти точкою 3D і не усунути проблематичний тип даних і запах зірваного методу? YMMV.
ben rudgers

10

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

Епіграма 9. Краще 100 функцій працювати на одній структурі даних, ніж 10 функцій на 10 структурах даних.

Той факт, що отримання права на «рівність» є такою проблемою, яка вночі підтримує винахідника «Скали» Мартина Ордерського, має зробити паузу щодо того, чи можна переосмислитиequals дерево на спадку - це гарна ідея.

Що трапляється, коли нам не пощастило отримати те ColoredPoint, що наша геометрія не вдається, тому що ми використовували спадщину для поширення типів даних, а не для того, щоб робити один хороший. Це, незважаючи на те, що потрібно повернутися та змінити кореневий вузол дерева спадкування, щоб зробити equalsроботу. Чому б просто не додати zі colorдо Point?

Важливою причиною було б це Pointта ColoredPointдіяти в різних областях ... принаймні, якщо ці домени ніколи не змішувалися. Але якщо це так, нам не потрібно перекривати equals. Порівняння ColoredPointта Pointрівність має сенс лише у третій галузі, де їм дозволено змішуватися. І в цьому випадку, мабуть, краще мати «рівність», пристосовану до цього третього домену, а не намагатися застосовувати семантику рівності з одного чи іншого або обох доменів, що не мають спокою. Іншими словами, "рівність" слід визначати локально до місця, куди в нас впадає грязь з обох сторін, тому що ми можемо не хотіти ColoredPoint.equals(pt)провалюватися проти випадків, Pointнавіть якщо автор ColoredPointподумав, що це було гарною ідеєю півроку тому о 2 ранку .


6

Коли старі боги програмування винайшли об'єктно-орієнтоване програмування з класами, вони вирішили, коли справа доходить до складу та успадкування мати два відношення для об'єкта: "є" і "має".
Це частково вирішило проблему, що підкласи відрізняються від батьківських класів, але зробила їх корисними, не порушуючи код. Оскільки екземпляр підкласу "є" об'єктом надкласового класу і його можна замінити безпосередньо, навіть незважаючи на те, що підклас має більше функцій-членів або членів даних, "має" гарантує, що він буде виконувати всі функції батьківського рівня і мати всі його члени. Отже, ви можете сказати, що Point3D "- це" точка, а Point2D "-" точка, якщо вони обидва успадковують від точки. Крім того, 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()або подібним), щоб досягти однакового ефекту.


1
Можна переосмислити хеш-код без перевищення рівних. Наприклад, можна зробити це для тестування іншого алгоритму хешування для тієї ж умови рівності.
Патрісія Шанахан

1

Часто буває корисно для непублічних звернених класів мати метод еквівалентності-тестування , який дозволяє об'єкти різних типів , щоб розглянути один одного «рівні» , якщо вони представляють ту ж саму інформацію, але тому , що Java не дозволяє ніяких коштів , з допомогою якого класи можуть видавати себе за кожен інше, часто добре мати єдиний тип обгортки, що знаходиться у відкритому доступі, у тих випадках, коли може бути можливість мати еквівалентні об'єкти з різними уявленнями.

Наприклад, розглянемо клас інкапсуляції незмінної 2D матриці doubleзначень. Якщо один зовнішній метод запитує матрицю ідентичності розміром 1000, другий запитує діагональну матрицю і передає масив, що містить 1000 одиниць, а третій запитує 2D матрицю і передає масив 1000x1000, де елементи на первинній діагоналі всі 1,0 а всі інші дорівнюють нулю, об'єкти, надані всім трьом класам, можуть використовувати різні резервні сховища внутрішньо [перший має єдине поле для розміру, другий має масив тисячі елементів, а третій має тисячу масивів 1000 елементів], але повинні повідомляти один одного як еквівалент [оскільки всі три інкапсулюють незмінну матрицю 1000x1000 з одиницями по діагоналі та нулями всюди в іншому].

Крім того, що він приховує існування різних типів резервного зберігання, обгортка також буде корисною для полегшення порівнянь, оскільки перевірка елементів на еквівалентність, як правило, є багатоетапним процесом. Запитайте перший предмет, чи знає він, чи дорівнює він другому; якщо він не знає, запитайте другого, чи знає він, чи рівний він першому. Якщо жоден об'єкт не знає, то запитайте кожен масив про вміст його окремих елементів [можна додати інші перевірки, перш ніж вирішити робити довгий і повільний маршрут порівняння окремих елементів].

Зауважте, що метод тестування на еквівалентність для кожного об'єкта в цьому сценарії повинен повернути значення три стану ("Так, я еквівалентний", "Ні, я не еквівалентний" або "Я не знаю"), тому нормальний метод "рівний" не підходить. Хоча будь-який об'єкт може просто відповісти "я не знаю", коли його запитують про будь-який інший, додаючи логіку до, наприклад, діагональної матриці, яка б не заважала запитувати будь-яку матрицю ідентифікації або діагональну матрицю про будь-які елементи з головної діагоналі, значно прискорить порівняння таких типи.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.