equals
sono hashCode
strettamente correlati tra loro e che è consigliabile sovrascrivere costantemente entrambi questi metodi nelle proprie classi. Un numero leggermente inferiore sa perché è così e quali tristi conseguenze possono verificarsi se questa regola viene infranta. Propongo di considerare il concetto di questi metodi, ripetere il loro scopo e capire perché sono così collegati. Ho scritto questo articolo, come il precedente sul caricamento delle classi, per me stesso per rivelare finalmente tutti i dettagli del problema e non tornare più a fonti di terze parti. Pertanto, sarò lieto di ricevere critiche costruttive, perché se ci sono lacune da qualche parte, dovrebbero essere colmate. L'articolo, ahimè, si è rivelato piuttosto lungo.
equivale a sovrascrivere le regole
In Java è necessario un metodoequals()
per confermare o negare il fatto che due oggetti con la stessa origine siano logicamente uguali . Cioè, quando si confrontano due oggetti, il programmatore deve capire se i loro campi significativi sono equivalenti . Non è necessario che tutti i campi siano identici, poiché il metodo equals()
implica l'uguaglianza logica . Ma a volte non è necessario utilizzare questo metodo. Come si suol dire, il modo più semplice per evitare problemi utilizzando un particolare meccanismo è non utilizzarlo. Va anche notato che una volta che si rompe un contratto, equals
si perde il controllo sulla comprensione di come altri oggetti e strutture interagiranno con il proprio oggetto. E successivamente sarà molto difficile trovare la causa dell'errore.
Quando non sovrascrivere questo metodo
- Quando ogni istanza di una classe è unica. In misura maggiore, ciò si applica a quelle classi che forniscono un comportamento specifico anziché essere progettate per funzionare con i dati. Tale, ad esempio, come la classe
- Quando in realtà la classe non è tenuta a determinare l'equivalenza delle sue istanze. Ad esempio, per una classe
- Quando la classe che stai estendendo ha già una propria implementazione del metodo
equals
e il comportamento di questa implementazione è adatto a te. Ad esempio, per le classi - Infine, non è necessario eseguire l'override
equals
quando l'ambito della classe èprivate
opackage-private
e sei sicuro che questo metodo non verrà mai chiamato.
Thread
. Per loro equals
l'implementazione del metodo fornito dalla classe Object
è più che sufficiente. Un altro esempio sono le classi enum ( Enum
).
java.util.Random
non è affatto necessario confrontare tra loro le istanze della classe, determinando se possono restituire la stessa sequenza di numeri casuali. Semplicemente perché la natura di questa classe non implica nemmeno un simile comportamento.
Set
, List
, Map
l'implementazione equals
è rispettivamente in AbstractSet
, AbstractList
e .AbstractMap
uguale al contratto
Quando si sovrascrive un metodo,equals
lo sviluppatore deve rispettare le regole di base definite nelle specifiche del linguaggio Java.
- Riflessività per ogni dato valore
- Simmetria per ogni dato valore
- Transitività per qualsiasi dato valore e
- Consistenza per qualsiasi valore specificato
- Confronto nullo per ogni dato valore
x
, l'espressione x.equals(x)
deve restituire true
.
Dato - il che significa che
x != null
x
e y
, x.equals(y)
dovrebbe restituire true
solo se y.equals(x)
restituisce true
.
x
, se restituisce e restituisce , deve restituire il valore . y
z
x.equals(y)
true
y.equals(z)
true
x.equals(z)
true
x
e y
la chiamata ripetuta x.equals(y)
restituirà il valore della chiamata precedente a questo metodo, a condizione che i campi utilizzati per confrontare i due oggetti non siano cambiati tra le chiamate.
x
la chiamata x.equals(null)
deve restituire false
.
equivale a violazione del contratto
Molte classi, come quelle del Java Collections Framework, dipendono dall'implementazione del metodoequals()
, quindi non dovresti trascurarlo, perché La violazione del contratto con questo metodo può portare a un funzionamento irrazionale dell'applicazione e in questo caso sarà abbastanza difficile trovarne il motivo. Secondo il principio di riflessività ogni oggetto deve essere equivalente a se stesso. Se questo principio viene violato, quando aggiungiamo un oggetto alla collezione e poi lo cerchiamo tramite il metodo, contains()
non riusciremo a trovare l'oggetto che abbiamo appena aggiunto alla collezione. La condizione di simmetria afferma che due oggetti qualsiasi devono essere uguali indipendentemente dall'ordine in cui vengono confrontati. Ad esempio, se hai una classe contenente un solo campo di tipo stringa, non sarà corretto confrontare equals
questo campo con una stringa in un metodo. Perché nel caso di confronto inverso il metodo restituirà sempre il valore false
.
// Нарушение симметричности
public class SomeStringify {
private String s;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof SomeStringify) {
return s.equals(((SomeStringify) o).s);
}
// нарушение симметричности, классы разного происхождения
if (o instanceof String) {
return s.equals(o);
}
return false;
}
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof SomeStringify &&
((SomeStringify) o).s.equals(s);
}
Dalla condizione di transitività segue che se due qualsiasi dei tre oggetti sono uguali, allora in questo caso tutti e tre devono essere uguali. Questo principio può essere facilmente violato quando è necessario estendere una certa classe base aggiungendovi un componente significativo . Ad esempio, ad una classe Point
con coordinate x
è y
necessario aggiungere il colore del punto espandendolo. Per fare ciò, dovrai dichiarare una classe ColorPoint
con l'apposito campo color
. Pertanto, se nella classe estesa chiamiamo il equals
metodo genitore e nel genitore assumiamo che vengano confrontate solo le coordinate x
e y
, allora due punti di colori diversi ma con le stesse coordinate saranno considerati uguali, il che non è corretto. In questo caso è necessario insegnare alla classe derivata a distinguere i colori. Per fare ciò, puoi utilizzare due metodi. Ma uno violerà la regola della simmetria , e la seconda quella della transitività .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
In questo caso, la chiamata point.equals(colorPoint)
restituirà il valore true
e il confronto colorPoint.equals(point)
restituirà false
, perché si aspetta un oggetto della “sua” classe. Pertanto, la regola della simmetria è violata. Il secondo metodo prevede l'esecuzione di un controllo “cieco” nel caso in cui non siano disponibili dati sul colore del punto, ovvero abbiamo la classe Point
. Oppure controlla il colore se sono disponibili informazioni al riguardo, ovvero confronta un oggetto della classe ColorPoint
.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
// Слепая проверка
if (!(o instanceof ColorPoint))
return super.equals(o);
// Полная проверка, включая цвет точки
return super.equals(o) && ((ColorPoint) o).color == color;
}
Il principio di transitività viene qui violato nel modo seguente. Diciamo che esiste una definizione dei seguenti oggetti:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Pertanto, sebbene l'uguaglianza p1.equals(p2)
e sia vera p2.equals(p3)
, p1.equals(p3)
restituirà il valore false
. Allo stesso tempo, il secondo metodo, secondo me, sembra meno attraente, perché In alcuni casi, l'algoritmo potrebbe essere cieco e non eseguire il confronto completamente e potresti non saperlo. Un po' di poesia In generale, a quanto ho capito, non esiste una soluzione concreta a questo problema. C'è un'opinione di un autorevole autore di nome Kay Horstmann secondo cui è possibile sostituire l'uso dell'operatore instanceof
con una chiamata al metodo getClass()
che restituisce la classe dell'oggetto e, prima di iniziare a confrontare gli oggetti stessi, assicurarsi che siano dello stesso tipo e non prestare attenzione al fatto della loro origine comune. Pertanto le regole di simmetria e transitività saranno soddisfatte. Ma allo stesso tempo, dall'altra parte della barricata c'è un altro autore, non meno rispettato in ampi ambienti, Joshua Bloch, il quale ritiene che questo approccio violi il principio di sostituzione di Barbara Liskov. Questo principio afferma che “il codice chiamante deve trattare una classe base allo stesso modo delle sue sottoclassi senza saperlo ” . E nella soluzione proposta da Horstmann questo principio è chiaramente violato, poiché da esso dipende l'attuazione. Insomma, è chiaro che la questione è oscura. Va anche notato che Horstmann chiarisce la regola per applicare il suo approccio e scrive in un inglese semplice che è necessario decidere una strategia quando si progettano le classi e se il test di uguaglianza verrà eseguito solo dalla superclasse, è possibile farlo eseguendo l'operazione instanceof
. Altrimenti, quando la semantica del controllo cambia a seconda della classe derivata e l'implementazione del metodo deve essere spostata più in basso nella gerarchia, è necessario utilizzare il metodo getClass()
. Joshua Bloch, a sua volta, propone di abbandonare l'ereditarietà e utilizzare la composizione di oggetti includendo una ColorPoint
classe nella classe Point
e fornendo un metodo di accesso asPoint()
per ottenere informazioni specifiche sul punto. Ciò eviterà di infrangere tutte le regole, ma, a mio avviso, renderà il codice più difficile da comprendere. La terza opzione consiste nell'utilizzare la generazione automatica del metodo equals utilizzando l'IDE. L'idea, tra l'altro, riproduce la generazione di Horstmann, consentendoti di scegliere una strategia per implementare un metodo in una superclasse o nei suoi discendenti. Infine, la successiva regola di coerenza afferma che, anche se gli oggetti x
non y
cambiano, richiamarli nuovamente x.equals(y)
deve restituire lo stesso valore di prima. La regola finale è che nessun oggetto dovrebbe essere uguale a null
. Qui tutto è chiaro null
: questa è incertezza, l'oggetto è uguale all'incertezza? Non è chiaro, cioè false
.
Algoritmo generale per determinare gli uguali
- Verificare l'uguaglianza dei riferimenti agli oggetti
this
e dei parametri del metodoo
.if (this == o) return true;
- Controllare se il collegamento è definito
o
, cioè se lo ènull
.
Se in futuro, quando si confrontano i tipi di oggetto, verrà utilizzato l'operatoreinstanceof
, questo elemento può essere saltato, poichéfalse
in questo caso questo parametro restituiscenull instanceof Object
. - Confronta i tipi di oggetto
this
utilizzandoo
un operatoreinstanceof
o un metodogetClass()
, guidato dalla descrizione sopra e dalla tua intuizione. - Se un metodo
equals
viene sovrascritto in una sottoclasse, assicurati di effettuare una chiamatasuper.equals(o)
- Convertire il tipo di parametro
o
nella classe richiesta. - Eseguire un confronto di tutti i campi oggetto significativi:
- per i tipi primitivi (eccetto
float
edouble
), utilizzando l'operatore==
- per i campi di riferimento è necessario chiamare il loro metodo
equals
- per gli array è possibile utilizzare l'iterazione ciclica o il metodo
Arrays.equals()
- per i tipi
float
eddouble
è necessario utilizzare metodi di confronto delle classi wrapper corrispondentiFloat.compare()
eDouble.compare()
- per i tipi primitivi (eccetto
- E infine, rispondi a tre domande: il metodo implementato è simmetrico ? Transitivo ? Concordato ? Gli altri due principi ( riflessività e certezza ) vengono solitamente eseguiti automaticamente.
HashCode sovrascrive le regole
Un hash è un numero generato da un oggetto che descrive il suo stato in un determinato momento. Questo numero viene utilizzato in Java principalmente nelle tabelle hash comeHashMap
. In questo caso, la funzione hash per ottenere un numero in base a un oggetto deve essere implementata in modo tale da garantire una distribuzione relativamente uniforme degli elementi nella tabella hash. E anche per ridurre al minimo la probabilità di collisioni quando la funzione restituisce lo stesso valore per chiavi diverse.
HashCode del contratto
Per implementare una funzione hash, la specifica del linguaggio definisce le seguenti regole:- chiamare un metodo
hashCode
una o più volte sullo stesso oggetto deve restituire lo stesso valore hash, a condizione che i campi dell'oggetto coinvolti nel calcolo del valore non siano cambiati. - chiamare un metodo
hashCode
su due oggetti dovrebbe sempre restituire lo stesso numero se gli oggetti sono uguali (chiamare un metodoequals
su questi oggetti restituiscetrue
). - la chiamata di un metodo
hashCode
su due oggetti non uguali deve restituire valori hash diversi. Sebbene questo requisito non sia obbligatorio, è opportuno considerare che la sua implementazione avrà un effetto positivo sulle prestazioni delle tabelle hash.
I metodi equals e hashCode devono essere sovrascritti insieme
In base ai contratti sopra descritti, ne consegue che quando si sovrascrive il metodo nel codiceequals
, è necessario sempre sovrascrivere il metodo hashCode
. Poiché infatti due istanze di una classe sono diverse perché si trovano in aree di memoria diverse, devono essere confrontate secondo alcuni criteri logici. Di conseguenza, due oggetti logicamente equivalenti devono restituire lo stesso valore hash. Cosa succede se solo uno di questi metodi viene sovrascritto?
-
equals
sihashCode
NoDiciamo che abbiamo definito correttamente un metodo
equals
nella nostra classe ehashCode
abbiamo deciso di lasciare il metodo così com'è nella classeObject
. Allora dal punto di vista del metodoequals
i due oggetti saranno logicamente uguali, mentre dal punto di vista del metodohashCode
non avranno nulla in comune. E quindi, inserendo un oggetto in una tabella hash, corriamo il rischio di non recuperarlo tramite chiave.
Ad esempio, in questo modo:Map<Point, String> m = new HashMap<>(); m.put(new Point(1, 1), “Point A”); // pointName == null String pointName = m.get(new Point(1, 1));
Ovviamente l'oggetto da posizionare e l'oggetto da cercare sono due oggetti diversi, anche se logicamente uguali. Ma perché hanno valori di hash diversi perché abbiamo violato il contratto, possiamo dire che abbiamo perso il nostro oggetto da qualche parte nelle viscere della tabella hash.
-
hashCode
siequals
No.Cosa succede se sovrascriviamo il metodo
hashCode
edequals
ereditiamo l'implementazione del metodo dalla classeObject
. Come sai, ilequals
metodo predefinito confronta semplicemente i puntatori con gli oggetti, determinando se si riferiscono allo stesso oggetto. Supponiamo dihashCode
aver scritto il metodo secondo tutti i canoni, ovvero di averlo generato utilizzando l'IDE, e restituirà gli stessi valori hash per oggetti logicamente identici. Ovviamente, così facendo abbiamo già definito un meccanismo per confrontare due oggetti.Pertanto, l'esempio del paragrafo precedente dovrebbe in teoria essere realizzato. Ma non saremo ancora in grado di trovare il nostro oggetto nella tabella hash. Anche se saremo vicini a questo, perché come minimo troveremo un cestino da hash table in cui giacerà l'oggetto.
Per cercare con successo un oggetto in una tabella hash, oltre a confrontare i valori hash della chiave, viene utilizzata anche la determinazione dell'uguaglianza logica della chiave con l'oggetto cercato. Cioè,
equals
non c'è modo di fare a meno di sovrascrivere il metodo.
Algoritmo generale per determinare hashCode
Qui, mi sembra, non dovresti preoccuparti troppo e generare il metodo nel tuo IDE preferito. Perché tutti questi spostamenti di bit a destra e a sinistra alla ricerca della sezione aurea, ad es. distribuzione normale, questo è per ragazzi completamente testardi. Personalmente dubito di poter fare meglio e più velocemente della stessa Idea.Invece di una conclusione
Vediamo quindi che i metodiequals
svolgono hashCode
un ruolo ben definito nel linguaggio Java e sono progettati per ottenere l'uguaglianza logica caratteristica di due oggetti. Nel caso del metodo, equals
questo ha una relazione diretta con il confronto di oggetti, nel caso di hashCode
un metodo indiretto, quando è necessario, diciamo, determinare la posizione approssimativa di un oggetto in tabelle hash o strutture dati simili per poter aumentare la velocità di ricerca di un oggetto. Oltre ai contratti , equals
esiste hashCode
un altro requisito legato al confronto degli oggetti. Questa è la coerenza di un metodo compareTo
di interfaccia Comparable
con un file equals
. Questo requisito obbliga lo sviluppatore a restituire sempre x.equals(y) == true
quando x.compareTo(y) == 0
. Cioè, vediamo che il confronto logico di due oggetti non dovrebbe contraddirsi in nessun punto dell'applicazione e dovrebbe essere sempre coerente.
GO TO FULL VERSION