Java: perché le collezioni accettano un comparatore ma non (un ipotetico) Hasher ed Equatore?


25

Questo problema è più evidente quando si hanno diverse implementazioni di un'interfaccia e, ai fini di una particolare raccolta, ci si preoccupa solo della vista a livello di interfaccia degli oggetti. Ad esempio, supponiamo di avere un'interfaccia come questa:

public interface Person {
    int getId();
}

Il solito modo di implementare hashcode()e equals()implementare le classi avrebbe un codice come questo nel equalsmetodo:

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

Ciò causa problemi quando si mescolano le implementazioni di Personin a HashMap. Se l' HashMapunico a preoccuparsi della vista a livello di interfaccia di Person, allora potrebbe finire con duplicati che differiscono solo nelle loro classi di implementazione.

Potresti far funzionare questo caso usando lo stesso equals()metodo liberale per tutte le implementazioni, ma corri il rischio di equals()fare la cosa sbagliata in un contesto diverso (come confrontare due Persons che sono supportate dai record del database con i numeri di versione).

La mia intuizione mi dice che l'uguaglianza dovrebbe essere definita per raccolta anziché per classe. Quando si utilizzano raccolte che si basano sull'ordinamento, è possibile utilizzare un'abitudine Comparatorper selezionare l'ordine corretto in ogni contesto. Non esiste un analogo per le raccolte basate su hash. Perchè è questo?

Giusto per chiarire, questa domanda è distinta da " Perché è .compareTo () in un'interfaccia mentre .equals () è in una classe in Java? " Perché si occupa dell'implementazione delle raccolte. compareTo()e equals()/ hashcode()entrambi soffrono del problema dell'universalità quando si usano le raccolte: non è possibile selezionare funzioni di confronto diverse per raccolte diverse. Quindi ai fini di questa domanda, la gerarchia ereditaria di un oggetto non ha alcuna importanza; tutto ciò che conta è se la funzione di confronto è definita per oggetto o per collezione.


5
È sempre possibile introdurre oggetti wrapper per Personimplementare l'atteso equalse il hashCodecomportamento. Avresti quindi un HashMap<PersonWrapper, V>. Questo è un esempio in cui un approccio puro-OOP non è elegante: non tutte le operazioni su un oggetto hanno senso come metodo di quell'oggetto. Intero Java Objecttipo è un amalgama di diverse responsabilità - solo la getClass, finalizee toStringmetodi sembrano lontanamente giustificabile dalle best practice di oggi.
amon,

1
1) In C # puoi passare un IEqualityComparer<T>a una raccolta basata sull'hash. Se non ne specifichi uno, utilizza un'implementazione predefinita basata su Object.Equalse Object.GetHashCode(). 2) L'override dell'IMO Equalssu un tipo di riferimento modificabile è raramente una buona idea. In questo modo l'uguaglianza di default è piuttosto rigida, ma puoi usare una regola di uguaglianza più rilassata quando ne hai bisogno tramite un'abitudine IEqualityComparer<T>.
CodesInChaos

Risposte:


23

Questo disegno è talvolta noto come "Uguaglianza universale", è la convinzione che due cose siano uguali o meno sia una proprietà universale.

Inoltre, l'uguaglianza è una proprietà di due oggetti, ma in OO si chiama sempre un metodo su un singolo oggetto e quell'oggetto decide solo come gestire quella chiamata di metodo. Quindi, in un progetto come quello di Java, dove l'uguaglianza è una proprietà di uno dei due oggetti confrontati, non è nemmeno possibile garantire alcune proprietà di base dell'uguaglianza come la simmetria ( a == bb == a), perché nel primo caso il metodo viene chiamato ae nel secondo caso viene richiesto be, in base ai principi di base dell'OO, è solo auna decisione (nel primo caso) obla decisione (nel secondo caso) di considerarsi o meno uguale all'altra. L'unico modo per ottenere la simmetria è far cooperare i due oggetti, ma se non ... la sfortuna.

Una soluzione sarebbe quella di rendere l'uguaglianza non una proprietà di un oggetto, ma una proprietà di due oggetti o una proprietà di un terzo oggetto. Quest'ultima opzione risolve anche il problema dell'uguaglianza universale, perché se si rende l'uguaglianza una proprietà di un terzo oggetto "contestuale", si può immaginare di avere EqualityCompareroggetti diversi per contesti diversi.

Questo è il design scelto per Haskell, ad esempio, con la Eqtypeclass. È anche il design scelto da alcune librerie Scala di terze parti (ScalaZ, ad esempio), ma non dal core Scala o dalla libreria standard, che utilizza l'uguaglianza universale per la compatibilità con la piattaforma host sottostante.

È, interessante, anche il design scelto con le interfacce Comparable/ Java Comparator. I progettisti di Java erano chiaramente a conoscenza del problema, ma per qualche ragione lo hanno risolto solo per l'ordinamento, ma non per l'uguaglianza (o hashing).

Quindi, per quanto riguarda la domanda

perché c'è Comparatorun'interfaccia ma no Hashere Equator?

la risposta è "Non lo so". Chiaramente, i progettisti di Java erano a conoscenza del problema, come evidenziato dall'esistenza di Comparator, ma ovviamente non pensavano che fosse un problema di uguaglianza e hashing. Altre lingue e librerie fanno scelte diverse.


7
+1, ma si noti che esistono lingue OO in cui sono presenti più invii (Smalltalk, Common Lisp). Quindi è sempre troppo forte nella seguente frase: "in OO, si chiama sempre un metodo su un singolo oggetto".
coredump,

Ho trovato la citazione che cercavo; secondo JLS 1.0, The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtablevale a dire entrambi equalse hashCodesono stati introdotti come Objectmetodi dagli sviluppatori Java esclusivamente per motivi di Hashtable- non esiste alcuna nozione di UE o di nulla silimar da nessuna parte nelle specifiche, e la citazione è abbastanza chiara per me; se non fosse per il Hashtable, equalsprobabilmente sarebbe stato in un'interfaccia simile Comparable. Come tale, mentre in precedenza credevo che la tua risposta fosse corretta, ora la considero infondata.
vaxquis,

@ JörgWMittag era un errore di battitura, IFTFY. A proposito, parlando clone- era originariamente un operatore , non un metodo (vedi la specifica della lingua di Oak), citando: 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)- i tre operatori simili a parole chiave erano instanceof new clone(sezione 8.1, operatori). Suppongo che questa sia la vera (storica) ragione del clone/ Cloneablepasticcio - Cloneablefu semplicemente un'invenzione successiva, e il clonecodice esistente fu adattato con esso.
vaxquis,

2
"Questo è il design scelto per Haskell, ad esempio, con la typeclass Eq" Questo è un po 'vero, ma vale la pena notare che Haskell afferma esplicitamente che due oggetti di tipi diversi non sono mai uguali mentre l'approccio di Java non lo fa. L'operazione di uguaglianza fa quindi parte del tipo , (quindi "typeclass") non parte di un terzo valore di contesto.
Jack,

19

La vera risposta a

perché c'è Comparatorun'interfaccia ma no Hashere Equator?

è, citazione per gentile concessione di Josh Bloch :

Le API Java originali sono state eseguite molto rapidamente in tempi ristretti per soddisfare una finestra di mercato di chiusura. Il team Java originale ha fatto un lavoro incredibile, ma non tutte le API sono perfette.

Il problema sta solo nella storia di Java, come per altre questioni simili, ad esempio .clone()vs Cloneable.

tl; dr

è principalmente per ragioni storiche; l'attuale comportamento / astrazione è stato introdotto in JDK 1.0 e non è stato corretto in seguito perché era praticamente impossibile farlo mantenendo la compatibilità con il codice a ritroso.


Innanzitutto, riassumiamo un paio di fatti Java noti:

  1. Java, dall'inizio ai giorni nostri, era orgogliosamente compatibile con le versioni precedenti, richiedendo che le API legacy fossero ancora supportate nelle versioni più recenti,
  2. in quanto tale, quasi ogni costrutto del linguaggio introdotto con JDK 1.0 è vissuto fino ai giorni nostri,
  3. Hashtable, .hashCode()E .equals()sono state realizzate in JDK 1.0, ( Hashtable )
  4. Comparable/ è Comparatorstato introdotto in JDK 1.2 ( comparabile ),

Ora segue:

  1. era praticamente impossibile e insensato retrofit .hashCode()e .equals()interfacce distinte, pur mantenendo la retrocompatibilità dopo che le persone si resero conto che ci sono astrazioni migliori che metterle in superoggetto, perché ad esempio tutti i programmatori Java di 1.2 sapevano che ognuno Objectli ha, e avevano rimanere lì fisicamente per fornire anche la compatibilità del codice compilato (JVM) - e aggiungere un'interfaccia esplicita a ogni Objectsottoclasse che le implementasse davvero renderebbe questo disordine uguale (sic!) a Clonableuno ( Bloch discute perché fa schifo Cloneable , anche discusso in ad esempio EJ 2nd e molti altri luoghi, incluso SO),
  2. li hanno appena lasciati lì per la generazione futura di avere una fonte costante di WTF.

Ora, potresti chiedere "che cosa Hashtableha tutto questo"?

La risposta è: hashCode()/ equals()contratto e abilità linguistiche non così buone degli sviluppatori Java principali nel 1995/1996.

Citazione da Java 1.0 Language Spec, datata 1996 - 4.3.2 The Class Object, p.41:

I metodi equalse hashCodesono dichiarati a beneficio di hashtables come java.util.Hashtable(§21.7). Il metodo equals definisce una nozione di uguaglianza di oggetti, che si basa sul confronto di valore, non di riferimento.

(nota che questa affermazione esatta è stata modificata nelle versioni successive, per esempio, citando:, The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.rendendo impossibile effettuare la connessione diretta Hashtable- hashCode- equalssenza leggere la JLS storica!)

Il team Java ha deciso di desiderare una buona raccolta in stile dizionario e ha creato Hashtable(buona idea finora), ma voleva che il programmatore fosse in grado di usarlo con il minor numero di curve di apprendimento / codice (oops! Guai in arrivo!) - e, poiché non vi era ancora [it del JDK 1.0, dopo tutto] non generici, ciò significherebbe che o ogni Object put in Hashtableavrebbe dovuto implementare esplicitamente alcuni di interfaccia (e le interfacce erano ancora solo nella loro inizio allora ... non Comparableancora, anche!) , rendendolo un deterrente per usarlo per molti - o Objectdovrebbe implementare implicitamente un metodo di hashing.

Ovviamente, sono andati con la soluzione 2, per i motivi indicati sopra. Sì, ora sappiamo che avevano torto. ... è facile essere intelligenti col senno di poi. ridacchiare

Ora, hashCode() richiede che ogni oggetto che lo possiede deve avere un equals()metodo distinto - quindi era abbastanza ovvio che equals()doveva essere inserito Objectanche.

Dal momento che i predefiniti implementazioni di questi metodi su valida a& b Objects sono sostanzialmente inutile per essere ridondante (rendendo a.equals(b) uguale a a==be a.hashCode() == b.hashCode() approssimativamente uguale a a==banche, a meno hashCodee / o equalsviene sovrascritto, oppure GC centinaia di migliaia di Objects durante il ciclo di vita dell'applicazione 1 ) , è sicuro di dire che sono stati forniti principalmente come misura di backup e per comodità d'uso. Questo è esattamente il modo in cui arriviamo al fatto ben noto che ha sempre la precedenza su entrambi .equals()e .hashCode()se intendete confrontare effettivamente gli oggetti o memorizzarli nell'hash. Sostituire solo uno di essi senza l'altro è un buon modo per rovinare il codice (con risultati di confronto malvagi o valori di collisione follemente elevati) - e aggirarlo è una fonte di costante confusione ed errori per i principianti (cerca SO per vedere per te) e fastidio costante a quelli più stagionati.

Inoltre, nota che sebbene C # gestisca in modo un po 'meglio eguali e hashcode, Eric Lippert stesso afferma di aver fatto quasi lo stesso errore con C # che Sun ha fatto con Java anni prima dell'inizio di C # :

Ma perché dovrebbe essere il caso che ogni oggetto dovrebbe essere in grado di eseguire l'hash per l'inserimento in una tabella hash? Sembra una cosa strana richiedere che ogni oggetto sia in grado di fare. Penso che se stessimo riprogettando il sistema dei tipi da zero oggi, l'hashing potrebbe essere fatto diversamente, forse con IHashableun'interfaccia. Ma quando è stato progettato il sistema di tipi CLR non c'erano tipi generici e quindi una tabella hash per scopi generici doveva essere in grado di memorizzare qualsiasi oggetto.

1 ovviamente, Object#hashCodepuò ancora scontrarsi, ma ci vuole un po 'di sforzo per farlo, vedi: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470 e rapporti sui bug collegati per i dettagli; /programming/1381060/hashcode-uniqueness/1381114#1381114 tratta questo argomento in modo più approfondito.


Non è solo Java, però. Molti dei suoi contemporanei (Ruby, Python, ...) e dei predecessori (Smalltalk, ...) e alcuni dei suoi successori hanno anche Uguaglianza Universale e Hashability Universale (è una parola?).
Jörg W Mittag,

@ JörgWMittag vedi programmers.stackexchange.com/questions/283194/… - Non sono d'accordo su "UE" in Java; Storicamente la UE non è mai stata una vera preoccupazione nella Objectprogettazione; l'hashbility era.
vaxquis,

@vaxquis Non voglio parlarne, ma il mio commento precedente mostra che due oggetti raggiungibili contemporaneamente possono avere lo stesso codice hash (predefinito).
Ripristina Monica il

1
@vaxquis OK. Lo compro. La mia preoccupazione è che qualcuno che sta imparando vedrà questo e penserà di essere intelligente usando l'hashcode di sistema invece di uguale a ecc. Se lo fanno, probabilmente funzionerà abbastanza bene tranne per le rare volte in cui non lo fa e ci sarà nessun modo per riprodurre il problema in modo affidabile.
JimmyJames,

1
Questa dovrebbe essere la risposta accettata, poiché la conclusione della risposta accettata è "non lo so"
Phoenix,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.