Ten problem jest najbardziej widoczny, gdy masz różne implementacje interfejsu, a do celów konkretnej kolekcji dbasz tylko o widok obiektów na poziomie interfejsu. Załóżmy na przykład, że masz taki interfejs:
public interface Person {
int getId();
}
Zwykły sposób implementacji hashcode()
i equals()
implementacji klas miałby taki kod w equals
metodzie:
if (getClass() != other.getClass()) {
return false;
}
Powoduje to problemy podczas mieszania implementacji Person
w HashMap
. Jeśli HashMap
zależy tylko na widoku na poziomie interfejsu Person
, może to oznaczać, że duplikaty różnią się jedynie klasami implementacyjnymi.
Możesz sprawić, by ten przypadek działał, stosując tę samą liberalną equals()
metodę dla wszystkich implementacji, ale wtedy ryzykujesz equals()
zrobienie niewłaściwej rzeczy w innym kontekście (np. Porównanie dwóch Person
s, które są wspierane przez rekordy bazy danych z numerami wersji).
Moja intuicja mówi mi, że równość powinna być zdefiniowana dla kolekcji zamiast dla klasy. Korzystając z kolekcji, które opierają się na zamawianiu, możesz użyć niestandardowego, Comparator
aby wybrać odpowiednie porządkowanie w każdym kontekście. Nie ma analogii dla kolekcji opartych na haszowaniu. Dlaczego to?
Aby wyjaśnić, to pytanie różni się od „ Dlaczego .compareTo () w interfejsie, podczas gdy .equals () jest w klasie w Javie? ”, Ponieważ dotyczy implementacji kolekcji. compareTo()
i equals()
/ i hashcode()
oba mają problem z uniwersalnością podczas korzystania z kolekcji: nie można wybrać różnych funkcji porównania dla różnych kolekcji. Na potrzeby tego pytania hierarchia dziedziczenia obiektu w ogóle nie ma znaczenia; liczy się tylko to, czy funkcja porównania jest zdefiniowana dla obiektu czy dla kolekcji.
źródło
Person
tej implementacji oczekiwanegoequals
ihashCode
zachowania. Miałbyś wtedyHashMap<PersonWrapper, V>
. Jest to przykład, w którym podejście oparte na czystym OOP nie jest eleganckie: nie każda operacja na obiekcie ma sens jako metoda tego obiektu. Cała JavaObject
typ jest amalgamatem różnych zadań - tylkogetClass
,finalize
atoString
metody wydają się uzasadnione zdalnie przez dzisiejszych najlepszych praktyk.IEqualityComparer<T>
kolekcję opartą na haszowaniu. Jeśli nie określisz, używa domyślnej implementacji opartej naObject.Equals
iObject.GetHashCode()
. 2) Nadpisywanie przez IMOEquals
zmiennego typu odniesienia rzadko jest dobrym pomysłem. W ten sposób domyślna równość jest dość surowa, ale możesz użyć bardziej swobodnej reguły równości, gdy potrzebujesz jej za pomocą niestandardowegoIEqualityComparer<T>
.Odpowiedzi:
Ten projekt jest czasem znany jako „Uniwersalna równość”, to przekonanie, że to, czy dwie rzeczy są równe, czy nie, jest uniwersalną własnością.
Co więcej, równość jest właściwością dwóch obiektów, ale w OO zawsze wywołujesz metodę na jednym obiekcie , a ten obiekt może jedynie decydować, jak obsłużyć to wywołanie metody. Tak więc w projekcie takim jak Java, w którym równość jest właściwością jednego z dwóch porównywanych obiektów, nie jest nawet możliwe zagwarantowanie niektórych podstawowych właściwości równości, takich jak symetria (
a == b
⇔b == a
), ponieważ w pierwszym przypadku metoda jest wzywany,a
aw drugim przypadku jest wzywanyb
i ze względu na podstawowe zasady OO, jest to wyłączniea
decyzja (w pierwszym przypadku) lubb
decyzja (w drugim przypadku), czy uważa się za równą drugiej. Jedynym sposobem na uzyskanie symetrii jest współpraca dwóch obiektów, ale jeśli nie… mają pecha.Jednym rozwiązaniem byłoby uczynienie z równości nie własności jednego obiektu, ale albo własności dwóch obiektów, albo własności trzeciego obiektu. Ta ostatnia opcja rozwiązuje również problem uniwersalnej równości, ponieważ jeśli uczynisz równość właściwością trzeciego obiektu „kontekstowego”, możesz sobie wyobrazić, że masz różne
EqualityComparer
obiekty dla różnych kontekstów.Jest to projekt wybrany dla Haskell, na przykład z
Eq
typem. Jest to również projekt wybrany przez niektóre biblioteki Scala innych firm (na przykład ScalaZ), ale nie rdzeń Scala lub standardowa biblioteka, która używa uniwersalnej równości dla kompatybilności z bazową platformą hosta.Co ciekawe, jest to również projekt wybrany z interfejsami Java
Comparable
/Comparator
. Projektanci Javy wyraźnie zdawali sobie sprawę z problemu, ale z jakiegoś powodu rozwiązali go tylko dla porządku, ale nie dla równości (lub mieszania).Co do pytania
odpowiedź brzmi „nie wiem”. Najwyraźniej projektanci Java byli świadomi problemu, o czym świadczy istnienie
Comparator
, ale oczywiście nie uważali tego za problem równości i mieszania. Inne języki i biblioteki dokonują różnych wyborów.źródło
The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtable
, czyli zarównoequals
ihashCode
zostały wprowadzone jakoObject
metod przez Java DEVS wyłącznie dla dobraHashtable
- nie ma pojęcia UE lub cokolwiek silimar gdziekolwiek w ciemno, a cytat jest wystarczająco jasne dla mnie; gdyby nieHashtable
,equals
prawdopodobnie byłby w takim interfejsieComparable
. Jako taki, chociaż wcześniej uważałem twoją odpowiedź za poprawną, teraz uważam ją za bezpodstawną.clone
- to był pierwotnie operator , a nie metoda (patrz Specyfikacja języka dębowego), cytat: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)
- trzema operatorami podobnymi do słów kluczowychinstanceof new clone
(sekcja 8.1, operatory). Zakładam, że to jest prawdziwa (historyczna) przyczynaclone
/Cloneable
mess -Cloneable
był to po prostu wynalazek późniejszy, a istniejącyclone
kod został z nim zmodernizowany.Prawdziwa odpowiedź na
cytat dzięki uprzejmości Josha Blocha :
Problem leży wyłącznie w historii Javy, podobnie jak w innych podobnych sprawach, np .
.clone()
Vs.Cloneable
tl; dr
dzieje się tak głównie z przyczyn historycznych; Obecne zachowanie / abstrakcja zostało wprowadzone w JDK 1.0 i nie zostało później naprawione, ponieważ było to praktycznie niemożliwe z zachowaniem kompatybilności kodu wstecznego.
Najpierw podsumujmy kilka dobrze znanych faktów o Javie:
Hashtable
,.hashCode()
i.equals()
zostały zaimplementowane w JDK 1.0, ( Hashtable )Comparable
/Comparator
został wprowadzony w JDK 1.2 ( porównywalny ),Teraz wygląda to następująco:
.hashCode()
i.equals()
wyróżnianie interfejsów przy jednoczesnym zachowaniu kompatybilności wstecznej po tym, jak ludzie zdali sobie sprawę, że istnieją lepsze abstrakcje niż umieszczanie ich w superobjektach, ponieważ np. każdy programista Java w wersji 1.2 wiedział, że każdyObject
je ma, i mieli pozostanie tam fizycznie w celu zapewnienia kompatybilności skompilowanego kodu (JVM) - i dodanie jawnego interfejsu do każdejObject
podklasy, która naprawdę je zaimplementowała, sprawiłoby, że ten bałagan byłby równy (sic!) doClonable
jednego ( Bloch omawia, dlaczego klonowanie jest do bani , również omówione np. w EJ 2nd i wiele innych miejsc, w tym SO),Teraz możesz zapytać „co
Hashtable
ma z tym wszystkim”?Odpowiedź brzmi:
hashCode()
/equals()
kontrakt i niezbyt dobre umiejętności projektowania języka przez głównych programistów Java w latach 1995/1996.Cytat ze specyfikacji języka Java 1.0, z 1996 r. - 4.3.2 The Class
Object
, str. 41:(uwaga dokładny ta wypowiedź została zmieniona w późniejszych wersjach, aby powiedzieć, cytat:
The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.
, uniemożliwiając, aby bezpośredniHashtable
-hashCode
-equals
połączenia bez czytania JLS historycznych!)Zespół Java zdecydował, że chce dobrej kolekcji w stylu słownika, i stworzył
Hashtable
(jak dotąd dobry pomysł), ale chciał, aby programista mógł go używać przy możliwie najmniejszej krzywej kodu / uczenia się (oops! Problemy z nadchodzącym!) - a ponieważ nie było jeszcze żadnych generycznych [to w końcu JDK 1.0], oznaczałoby to, że każdyObject
włożonyHashtable
musiałby jawnie zaimplementować jakiś interfejs (a interfejsy były wtedy jeszcze w początkowej fazie ...Comparable
nawet jeszcze!) , co zniechęca do korzystania z niego przez wiele osób - lubObject
musiałoby niejawnie zaimplementować jakąś metodę haszującą.Oczywiście wybrali rozwiązanie 2 z powodów przedstawionych powyżej. Tak, teraz wiemy, że się mylili. ... z perspektywy czasu łatwo być mądrym. chichot
Teraz
hashCode()
wymaga, aby każdy obiekt, który go posiada, musiał mieć odrębnąequals()
metodę - więc było całkiem oczywiste, żeequals()
trzeba go również wprowadzićObject
.Ponieważ domyślne implementacje tych metod na ważność
a
ib
Object
s są w zasadzie bezużyteczne, będąc zbędny (coa.equals(b)
równa sięa==b
ia.hashCode() == b.hashCode()
mniej więcej równa sięa==b
również, chyba żehashCode
i / lubequals
jest nadpisane, albo GC setki tysięcyObject
s podczas całego cyklu życia aplikacji 1 ) , można bezpiecznie powiedzieć, że zostały one dostarczone głównie jako środek zapasowy i dla wygody użytkowania. Właśnie w ten sposób dochodzimy do dobrze znanego faktu, który zawsze zastępuje oba.equals()
i.hashCode()
jeśli zamierzasz faktycznie porównywać obiekty lub przechowywać je w postaci skrótów. Zastąpienie tylko jednego z nich bez drugiego jest dobrym sposobem na wkręcenie kodu (przez nikczemne porównanie wyników lub niesamowicie wysokie wartości kolizji z wiadrem) - a obejście go jest źródłem ciągłego zamieszania i błędów dla początkujących (wyszukaj SO, aby zobaczyć to dla siebie) i ciągłe uciążliwości dla bardziej doświadczonych.Zauważ też, że chociaż C # radzi sobie z equals i hashcode w nieco lepszy sposób, sam Eric Lippert twierdzi, że popełnił prawie taki sam błąd z C #, jaki Sun zrobił z Javą na wiele lat przed początkiem C # :
1 oczywiście
Object#hashCode
nadal może się kolidować, ale zajmuje to trochę wysiłku, patrz: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470 i powiązane raporty błędów, aby uzyskać szczegółowe informacje; /programming/1381060/hashcode-uniqueness/1381114#1381114 obejmuje ten temat bardziej szczegółowo.źródło
Object
projektowaniu; hashability was.