Java: чому колекції приймають компаратор, але не (гіпотетичний) хешер та екватор?


25

Ця проблема найбільш очевидна, коли у вас є різні реалізації інтерфейсу, і для цілей конкретної колекції ви дбаєте лише про перегляд об'єктів на рівні інтерфейсу. Наприклад, припустимо, у вас був такий інтерфейс:

public interface Person {
    int getId();
}

Звичайний спосіб реалізації hashcode()та equals()в реалізації класів мав би такий код у equalsметоді:

if (getClass() != other.getClass()) {
    return false;
}

Це спричиняє проблеми, коли ви змішуєте реалізацію Personв HashMap. Якщо питання стосується HashMapлише інтерфейсу на рівні інтерфейсу Person, то це може призвести до дублікатів, які відрізняються лише класами реалізації.

Ви можете зробити так, щоб ця справа працювала, використовуючи один і той же ліберальний equals()метод для всіх реалізацій, але тоді ви ризикуєте equals()зробити неправильну справу в іншому контексті (наприклад, порівнюючи два Persons, які підтримуються записами бази даних з номерами версій).

Моя інтуїція підказує мені, що рівність слід визначати за колекцією, а не за класом. Під час використання колекцій, які залежать від замовлення, ви можете скористатися користувачем, Comparatorщоб вибрати правильне замовлення у кожному контексті. Не існує аналога для колекцій на основі хешу. Чому це?

Просто для уточнення це питання відрізняється від " Чому .compareTo () в інтерфейсі, а .equals () є класом на Java? ", Оскільки воно стосується реалізації колекцій. compareTo()та equals()/ hashcode()і обидва страждають від проблеми універсальності при використанні колекцій: ви не можете вибрати різні функції порівняння для різних колекцій. Отже для цілей цього питання ієрархія спадкування об'єкта не має значення; Важливо лише те, чи визначена функція порівняння на об'єкт чи на колекцію.


5
Ви завжди можете вводити об'єкти обгортки для того, Personщоб реалізувати очікувані equalsта hashCodeповедінку. Тоді ви мали б HashMap<PersonWrapper, V>. Це один із прикладів, коли підхід «чистого OOP» не є елегантним: не кожна операція над об’єктом має сенс як метод цього об’єкта. Весь Objectтип Java - це сукупність різних обов'язків - лише методи getClass, finalizeі toStringметоди здаються виправданими віддаленими сучасними найкращими методами.
амон

1
1) У C # ви можете передати IEqualityComparer<T>колекцію на основі хешу. Якщо його не вказати, він використовує реалізацію за замовчуванням на основі Object.Equalsта Object.GetHashCode(). 2) Переоцінка IMO Equalsна змінний тип посилання рідко є доброю ідеєю. Таким чином, рівність за замовчуванням є досить суворою, але ви можете використовувати більш розслаблене правило рівності, коли це потрібно через звичай IEqualityComparer<T>.
CodesInChaos

2
Пов'язане мета-запитання: чи ці запитання дублікати між собою?

Відповіді:


23

Ця конструкція іноді відома як «Універсальна рівність», це переконання, що дві речі рівні чи ні - це універсальна властивість.

Більше того, рівність є властивістю двох об'єктів, але в OO ви завжди викликаєте метод на одному об'єкті , і цей об'єкт повинен вирішувати, як керувати цим викликом методу. Отже, у такому дизайні, як Java, де рівність є властивістю одного з двох об'єктів, що порівнюються, навіть неможливо гарантувати деякі основні властивості рівності, такі як симетрія ( a == bb == a), оскільки в першому випадку метод закликається, aа у другому випадку його закликають, bі в силу основних принципів ОО, це виключно aрішення (у першому випадку) абоbрішення (у другому випадку), чи вважає він себе рівним іншому. Єдиний спосіб здобути симетрію - це змусити два об'єкти співпрацювати, але якщо вони не… міцна удача.

Одним з рішень було б зробити рівність не властивістю одного об'єкта, а або властивістю двох об'єктів, або властивістю третього об'єкта. Цей останній варіант також вирішує проблему загальної рівності, тому що якщо ви зробите рівність властивістю третього "контекстного" об'єкта, то ви можете уявити, що EqualityComparerу різних контекстах є різні об'єкти.

Це є конструкція обрана для Haskell, наприклад, з Eqкласом типів. Це також дизайн, обраний деякими сторонніми бібліотеками Scala (наприклад, ScalaZ), але не ядро ​​Scala або стандартна бібліотека, яка використовує універсальну рівність для сумісності з базовою платформою хоста.

Цікаво, що також дизайн, обраний із Java Comparable/ Comparatorінтерфейсами. Дизайнери Java чітко усвідомлювали проблему, але чомусь вирішили її лише для того, щоб замовити, але не для рівності (або хеширования).

Отже, що стосується питання

чому є Comparatorінтерфейс, але немає Hasherі Equator?

відповідь - "я не знаю". Зрозуміло, що дизайнери Java знали про проблему, про що свідчить існування Comparator, але вони, очевидно, не вважали це проблемою для рівності та хеширования. Інші мови та бібліотеки роблять різні варіанти.


7
+1, але зауважте, що існують мови OO, де існує декілька відправок (Smalltalk, Common Lisp). Тому завжди занадто сильно в наступному реченні: "в ОО, ви завжди викликаєте метод на одному об'єкті".
coredump

Я знайшов цитату, яку я шукав; згідно з JLS 1.0, The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtableтобто обидва equalsі hashCodeбули запроваджені як Objectметоди Java-розробниками виключно заради Hashtable- ніде в специфікації немає поняття UE або чогось силімару, і цитата для мене досить чітка; якби не той Hashtable, equalsнапевно, був би в інтерфейсі, як Comparable. Тому я вважав, що ваша відповідь є правильною, але зараз я вважаю її необґрунтованою.
vaxquis

@ JörgWMittag це був помилковий помилок, IFTFY. BTW, кажучи про clone- спочатку це був оператор , а не метод (див. Специфікацію мови дуба), цитата: The unary operator clone is applied to an object. (...) The clone operator is normally used inside new to clone the prototype of some class, before applying the initializers (constructors)- три оператори, подібні до ключових слів, були instanceof new clone(розділ 8.1, оператори). Я припускаю, що це справжня (історична) причина clone/ Cloneableбезлад - Cloneableбула просто пізнішим винаходом, і існуючий cloneкод був переоснащений цим.
vaxquis

2
"Це дизайн, обраний для Haskell, наприклад, з типним класом Eq" Це начебто правда, але варто відзначити, що Haskell прямо заявляє, що два об'єкти різних типів ніколи не рівні, тоді як підхід Java не відповідає. Операція рівності, таким чином, є частиною типу (отже, "typeclass") не є частиною третього контекстного значення.
Джек

19

Справжня відповідь на

чому є Comparatorінтерфейс, але немає Hasherі Equator?

є, цитувати люб'язно Джоша Блоха :

Оригінальні API API Java були зроблені дуже швидко в стислі терміни, щоб зустріти закриття вікна ринку. Оригінальна команда Java зробила неймовірну роботу, але не всі API є ідеальними.

Проблема полягає тільки в історії Яви, як і з іншими подібними питаннями, наприклад , .clone()проти Cloneable.

тл; д-р

це в основному з історичних причин; Поточна поведінка / абстракція була введена в JDK 1.0 і не була виправлена ​​згодом, оскільки це було практично неможливо зробити при підтримці сумісності із зворотним кодом.


Спочатку підведемо підсумки декількох відомих фактів Java:

  1. Java з самого початку до сьогодні була гордо сумісною, вимагаючи, щоб застарілі API підтримувались у нових версіях,
  2. як такий, майже кожна мовна конструкція, представлена ​​JDK 1.0, жила донині,
  3. Hashtable, .hashCode()та .equals()були реалізовані в JDK 1.0, ( Hashtable )
  4. Comparable/ Comparatorбув представлений у JDK 1.2 ( Порівняно ),

Тепер випливає:

  1. модернізувати .hashCode()та .equals()розрізнити інтерфейси було практично неможливо і безглуздо, зберігаючи сумісність із зворотним бажанням після того, як люди зрозуміли, що є кращі абстракції, ніж ставити їх у суперпроект, тому що, наприклад, кожен програміст Java на 1,2 знав, що кожен Objectмає їх, і вони мали залишатися там фізично, щоб забезпечити сумісність компільованого коду (JVM) - і додавання явного інтерфейсу до кожного Objectпідкласу, який реально їх реалізував, зробить цей безлад рівним (sic!) Clonableодному ( Bloch обговорює, чому Cloneable смокче , також обговорюється, наприклад, у EJ 2nd та багато інших місць, включаючи SO),
  2. вони просто залишили їх там, щоб майбутнє покоління було постійним джерелом WTF.

Тепер ви можете запитати "що Hashtableмає все це"?

Відповідь: hashCode()/ equals()контракт і не дуже хороші навички мовного дизайну основних розробників Java у 1995/1996 роках.

Цитата з мовної специфікації Java 1.0, від 1996 р. - 4.3.2 Клас Object, стор.41:

Методи equalsі hashCodeдекларуються на користь хеш- java.util.Hashtableфайлів, таких як (§21.7). Метод дорівнює визначає поняття об’єктної рівності, яке базується на значенні, а не посиланні, порівнянні.

(зауважте, що це точне твердження було змінено в пізніших версіях, скажімо, цитата:, що The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.робить неможливим встановлення прямого Hashtable- hashCode- equalsз'єднання без читання історичних JLS!)

Команда Java вирішила, що хоче гарну колекцію стильового словника, і вони створили Hashtable(хороша ідея поки що), але вони хотіли, щоб програміст міг використовувати її з якомога меншою кривою коду / навчання (на жаль! Проблеми з вхідними!) - і, оскільки ще не було жодної генерики (все-таки JDK 1.0), це означатиме, що кожен Object з Hashtableних повинен явно реалізовувати якийсь інтерфейс (а інтерфейси тоді ще були лише на початку) ... Comparableще немає!) , що робить це стримуючим фактором, щоб використовувати його для багатьох - або Objectдоведеться неявно реалізувати якийсь метод хешування.

Очевидно, вони йшли з рішенням 2 з причин, зазначених вище. Так, тепер ми знаємо, що вони помилялися. ... легко бути розумним заднім числом. глузувати

Тепер, hashCode() вимагає, щоб кожен об'єкт, що має його, повинен мати чіткий equals()метод - тому цілком очевидно, що equals()його потрібно було також вкласти Object.

Оскільки по замовчуванням реалізацій цих методів по дійсним aі b Objectз, за суті марна, будучи надлишковими (робить a.equals(b) рівними для a==bі a.hashCode() == b.hashCode() приблизно рівні по a==bтакож, якщо hashCodeі / або equalsне перекриває, або GC сотні тисяч Objectз в протягом всього життєвого циклу додатки 1 ) , можна з упевненістю сказати, що вони надавалися в основному як міра резервного копіювання та для зручності використання. Саме так ми доходимо до загальновідомого факту, який завжди переосмислює обидва, .equals()і .hashCode()якщо ви маєте намір насправді порівнювати об'єкти або зберігати їх хеш-пам'ять. Переосмислення лише одного з них без іншого - це хороший спосіб накрутити свій код (за злісні порівняння результатів або шалено високі значення зіткнення ковша) - і обернути голову навколо цього - джерело постійної плутанини та помилок для початківців (шукайте ТАК, щоб побачити це для себе) і постійна неприємність до більш досвідчених.

Також зауважте, що хоча C # має справу з рівними та хеш-кодами дещо кращим чином, сам Ерік Ліпперт заявляє, що вони зробили майже таку саму помилку з C #, що Sun робила з Java за роки до початку C # :

Але чому має бути так, що кожен об'єкт повинен мати можливість хешувати себе для вставки в хеш-таблицю? Здається, що це дивна річ, що вимагає, щоб кожен об’єкт міг це зробити. Я думаю, якби ми сьогодні переробляли систему типів з нуля, хешування може бути зроблено інакше, можливо, з IHashableінтерфейсом. Але коли система CLR була розроблена, не було загальних типів, а тому хеш-таблиця загального призначення, необхідна для зберігання будь-якого об'єкта.

1 , звичайно, Object#hashCodeвсе ще може вступити в протиріччя, але це займає трохи зусиль , щоб зробити це, см: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470 і пов'язані повідомлення про помилки для деталей; /programming/1381060/hashcode-uniqueness/1381114#1381114 висвітлює цю тему більш глибоко.


Це не лише Java. Багато його сучасників (Ruby, Python,…) та попередники (Smalltalk,…), а також деякі його наступники також мають Універсальну рівність та Універсальну придатність (це слово?).
Йорг W Міттаг

@ JörgWMittag див. Programmers.stackexchange.com/questions/283194/… - я не погоджуюся з приводу "UE" на Java; UE історично ніколи не викликала побоювань у Objectдизайні Росії; зручність була.
vaxquis

@vaxquis Я не хочу цього робити, але мій попередній коментар показує, що два одночасно доступні об'єкти можуть мати однаковий хеш-код (за замовчуванням).
Відновіть Моніку

1
@vaxquis ОК. Я купую це. Мене хвилює те, що хтось, хто навчається, побачить це і подумає, що він розумний, використовуючи системний хеш-код замість рівних і т.д. ніякого способу відтворити проблему надійно.
JimmyJames

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