JavaRush /Java Blog /Random-IT /Contratti Equals e hashCode o qualunque cosa sia
Aleksandr Zimin
Livello 1
Санкт-Петербург

Contratti Equals e hashCode o qualunque cosa sia

Pubblicato nel gruppo Random-IT
La stragrande maggioranza dei programmatori Java, ovviamente, sa che i metodi equalssono hashCodestrettamente 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 metodo equals()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, equalssi 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 Thread. Per loro equalsl'implementazione del metodo fornito dalla classe Objectè più che sufficiente. Un altro esempio sono le classi enum ( Enum).
  • Quando in realtà la classe non è tenuta a determinare l'equivalenza delle sue istanze.
  • Ad esempio, per una classe java.util.Randomnon è 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.
  • Quando la classe che stai estendendo ha già una propria implementazione del metodo equalse il comportamento di questa implementazione è adatto a te.
  • Ad esempio, per le classi Set, List, Mapl'implementazione equalsè rispettivamente in AbstractSet, AbstractListe .AbstractMap
  • Infine, non è necessario eseguire l'override equalsquando l'ambito della classe è privateo package-privatee sei sicuro che questo metodo non verrà mai chiamato.

uguale al contratto

Quando si sovrascrive un metodo, equalslo sviluppatore deve rispettare le regole di base definite nelle specifiche del linguaggio Java.
  • Riflessività
  • per ogni dato valore x, l'espressione x.equals(x)deve restituire true.
    Dato - il che significa chex != null
  • Simmetria
  • per ogni dato valore xe y, x.equals(y)dovrebbe restituire truesolo se y.equals(x)restituisce true.
  • Transitività
  • per qualsiasi dato valore e x, se restituisce e restituisce , deve restituire il valore . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Consistenza
  • per qualsiasi valore specificato xe yla 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.
  • Confronto nullo
  • per ogni dato valore xla chiamata x.equals(null)deve restituire false.

equivale a violazione del contratto

Molte classi, come quelle del Java Collections Framework, dipendono dall'implementazione del metodo equals(), 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 equalsquesto 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 Pointcon coordinate xè ynecessario aggiungere il colore del punto espandendolo. Per fare ciò, dovrai dichiarare una classe ColorPointcon l'apposito campo color. Pertanto, se nella classe estesa chiamiamo il equalsmetodo genitore e nel genitore assumiamo che vengano confrontate solo le coordinate xe 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 truee 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 instanceofcon 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 ColorPointclasse nella classe Pointe 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 xnon ycambiano, 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

  1. Verificare l'uguaglianza dei riferimenti agli oggetti thise dei parametri del metodo o.
    if (this == o) return true;
  2. Controllare se il collegamento è definito o, cioè se lo è null.
    Se in futuro, quando si confrontano i tipi di oggetto, verrà utilizzato l'operatore instanceof, questo elemento può essere saltato, poiché falsein questo caso questo parametro restituisce null instanceof Object.
  3. Confronta i tipi di oggetto thisutilizzando oun operatore instanceofo un metodo getClass(), guidato dalla descrizione sopra e dalla tua intuizione.
  4. Se un metodo equalsviene sovrascritto in una sottoclasse, assicurati di effettuare una chiamatasuper.equals(o)
  5. Convertire il tipo di parametro onella classe richiesta.
  6. Eseguire un confronto di tutti i campi oggetto significativi:
    • per i tipi primitivi (eccetto floate double), utilizzando l'operatore==
    • per i campi di riferimento è necessario chiamare il loro metodoequals
    • per gli array è possibile utilizzare l'iterazione ciclica o il metodoArrays.equals()
    • per i tipi floated doubleè necessario utilizzare metodi di confronto delle classi wrapper corrispondenti Float.compare()eDouble.compare()
  7. 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 come HashMap. 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 hashCodeuna 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 hashCodesu due oggetti dovrebbe sempre restituire lo stesso numero se gli oggetti sono uguali (chiamare un metodo equalssu questi oggetti restituisce true).
  • la chiamata di un metodo hashCodesu 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 codice equals, è 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?
  1. equalssi hashCodeNo

    Diciamo che abbiamo definito correttamente un metodo equalsnella nostra classe e hashCodeabbiamo deciso di lasciare il metodo così com'è nella classe Object. Allora dal punto di vista del metodo equalsi due oggetti saranno logicamente uguali, mentre dal punto di vista del metodo hashCodenon 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.

  2. hashCodesi equalsNo.

    Cosa succede se sovrascriviamo il metodo hashCodeed equalsereditiamo l'implementazione del metodo dalla classe Object. Come sai, il equalsmetodo predefinito confronta semplicemente i puntatori con gli oggetti, determinando se si riferiscono allo stesso oggetto. Supponiamo di hashCodeaver 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è, equalsnon 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 metodi equalssvolgono hashCodeun ruolo ben definito nel linguaggio Java e sono progettati per ottenere l'uguaglianza logica caratteristica di due oggetti. Nel caso del metodo, equalsquesto ha una relazione diretta con il confronto di oggetti, nel caso di hashCodeun 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 , equalsesiste hashCodeun altro requisito legato al confronto degli oggetti. Questa è la coerenza di un metodo compareTodi interfaccia Comparablecon un file equals. Questo requisito obbliga lo sviluppatore a restituire sempre x.equals(y) == truequando 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.

Fonti

Java efficace, seconda edizione. Joshua Bloch. Traduzione gratuita di un ottimo libro. Java, una libreria professionale. Volume 1. Nozioni di base. Kay Horstmann. Un po’ meno teoria e più pratica. Ma tutto non viene analizzato così dettagliatamente come in Bloch. Sebbene esista una vista sullo stesso equals(). Strutture dati nelle immagini. HashMap Un articolo estremamente utile sul dispositivo HashMap in Java. Invece di guardare le fonti.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION