Perché sovrascrivere i metodi equals e hashcode in Java?
Fonte:
Medium Questo articolo si concentra su due metodi strettamente correlati: equals() e hashcode() . Imparerai come interagiscono tra loro e come sovrascriverli correttamente.
Perché sovrascriviamo il metodo equals()?
In Java, non possiamo sovraccaricare il comportamento di operatori come
== ,
+= ,
-+ . Funzionano secondo un determinato processo. Consideriamo ad esempio il funzionamento dell'operatore
== .
Come funziona l'operatore ==?
Controlla se i due riferimenti confrontati puntano alla stessa istanza in memoria. L' operatore
== valuterà true solo se i due riferimenti rappresentano la stessa istanza in memoria. Diamo un'occhiata al codice di esempio:
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
}
Diciamo che nel tuo programma hai creato due oggetti
Persona in posti diversi e vuoi confrontarli.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println( person1 == person2 ); --> will print false!
Dal punto di vista aziendale, i due sembrano uguali, giusto? Ma per la JVM non sono la stessa cosa. Poiché entrambe vengono create utilizzando la parola chiave
new , queste istanze si trovano in segmenti di memoria diversi. Pertanto l' operatore
== restituirà
false . Ma se non possiamo sovrascrivere l' operatore
== , allora come diciamo alla JVM che vogliamo che questi due oggetti siano trattati allo stesso modo? È qui che entra in gioco
il metodo .equals() . Puoi sovrascrivere
equals() per verificare se alcuni oggetti hanno gli stessi valori per determinati campi in modo da considerarli uguali. Puoi scegliere quali campi confrontare. Se diciamo che due oggetti
Person saranno uguali solo se hanno la stessa età e lo stesso nome, allora l'IDE genererà qualcosa di simile per creare automaticamente
equals() :
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
Torniamo al nostro esempio precedente.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
System.out.println ( person1.equals(person2) ); --> will print true!
Sì, non possiamo sovraccaricare l' operatore
== per confrontare gli oggetti nel modo che vogliamo, ma Java ci offre un altro modo: il metodo
equals() , che possiamo sovrascrivere come desideriamo.
Tieni presente che se non forniamo la nostra versione personalizzata di .equals() (nota anche come override) nella nostra classe, allora .equals() predefinito dalla classe Object e l' operatore == si comporteranno allo stesso modo. Il metodo
equals() predefinito , ereditato da
Object , controllerà se entrambe le istanze confrontate sono le stesse in memoria!
Perché stiamo sovrascrivendo il metodo hashCode()?
Alcune strutture dati in Java, come
HashSet e
HashMap , memorizzano i propri elementi in base a una funzione hash applicata a tali elementi. La funzione hash è
hashCode() . Se possiamo scegliere di sovrascrivere
il metodo .equals() , allora dovremmo avere anche la possibilità di sovrascrivere
il metodo hashCode() . C'è una ragione per questo. Dopotutto, l'implementazione predefinita
di hashCode() , ereditata da
Object , considera unici tutti gli oggetti in memoria! Ma torniamo a queste strutture di dati hash. Esiste una regola per queste strutture dati.
Un HashSet non può contenere valori duplicati e una HashMap non può contenere chiavi duplicate. Un HashSet viene implementato utilizzando
una HashMap in modo tale che ogni valore
HashSet venga archiviato come chiave in
HashMap . Come funziona
HashMap ?
HashMap è un array nativo con più segmenti. Ogni segmento ha un elenco collegato (
linkedList ). Questo elenco collegato memorizza le nostre chiavi.
HashMap trova la linkedList corretta per ciascuna chiave utilizzando il metodo
hashCode() , quindi scorre tutti gli elementi di quella
linkedList e applica il metodo
equals() a ciascuno di questi elementi per verificare se quell'elemento è contenuto lì.
Non sono ammesse chiavi duplicate. Quando inseriamo qualcosa in
una HashMap , la chiave viene memorizzata in uno di questi elenchi collegati. In quale elenco collegato verrà archiviata questa chiave viene mostrato dal risultato del metodo
hashCode() per quella chiave. Cioè, se
key1.hashCode() restituisce 4, allora
key1 verrà archiviato nel quarto segmento dell'array nella
LinkedList esistente lì . Per impostazione predefinita, il metodo
hashCode() restituisce risultati diversi per ciascuna istanza. Se abbiamo un
equals() predefinito che si comporta come
== , trattando tutte le istanze in memoria come oggetti diversi, allora non ci saranno problemi. Come ricorderete, nel nostro esempio precedente abbiamo detto che vogliamo che le istanze
di Person siano considerate uguali se la loro età e i loro nomi sono gli stessi.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1.equals(person2) ); --> will print true!
Ora creiamo una mappa per archiviare queste istanze come chiavi con una stringa specifica come coppia di valori.
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
Nella classe
Person , non abbiamo sovrascritto il metodo
hashCode , ma abbiamo un metodo
equals sovrascritto . Poiché l'
hashCode predefinito fornisce risultati diversi per diverse istanze Java
di person1.hashCode() e
person2.hashCode() , ci sono alte probabilità di ottenere risultati diversi. La nostra mappa può terminare con
persone diverse in diversi elenchi collegati.
Questo va contro la logica
di HashMap .
Dopotutto, una HashMap non può avere più chiavi identiche! Il punto è che l'
hashCode() predefinito ereditato dalla classe
Object non è sufficiente. Anche dopo aver sovrascritto il metodo
equals() della classe
Person . Ecco perché dobbiamo sovrascrivere il metodo
hashCode() dopo aver sovrascritto il metodo
equals . Ora risolviamo questo problema. Dobbiamo sovrascrivere il nostro metodo
hashCode() in modo che prenda in considerazione gli stessi campi di
equals() , ovvero
age e
name .
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
@Override
public int hashCode() {
int prime = 31;
return prime*Objects.hash(name, age);
}
Nel metodo
hashCode() abbiamo utilizzato un valore semplice (puoi utilizzare qualsiasi altro valore). Tuttavia, si suggerisce di utilizzare numeri primi per creare meno problemi. Proviamo di nuovo a memorizzare queste chiavi nella nostra
HashMap :
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
person1.hashCode() e
person2.hashCode() saranno gli stessi. Diciamo che sono 0.
La HashMap andrà al segmento 0 e in esso
la LinkedList salverà
persona1 come chiave con il valore “1”. Nel secondo caso, quando
HashMap va nuovamente al bucket 0 per memorizzare la chiave
person2 con il valore “2”, vedrà che lì esiste già un’altra chiave uguale ad essa. In questo modo sovrascriverà la chiave precedente. E solo la chiave
person2 esisterà nella nostra
HashMap . Ecco come abbiamo imparato come funziona la regola
HashMap , che afferma che non è possibile utilizzare più chiavi identiche!
Tuttavia, tieni presente che istanze diverse possono avere lo stesso hashcode e istanze uguali devono restituire lo stesso hashcode.
GO TO FULL VERSION