Uno degli argomenti classici di un qualunque corso Java che si rispetti è come capire se due oggetti sono uguali. Tutti starete pensando: che ci vuole, basta usare il metodo equals
di Object
! In effetti l’argomento può sembrare molto semplice, ma secondo me se ci fosse una classifica delle cause più frequenti di bug il non corretto utilizzo del metodo equals
sarebbe nelle prime posizioni!
Tipi primitivi Vs oggetti
Iniziamo con una breve introduzione a uno dei concetti chiave di Java: si sente dire spesso l’affermazione in Java tutto è un oggetto. In realtà questa frase non è del tutto vera in quanto esistono i tipi primitivi che permettono di utilizzare numeri, caratteri e valori booleani senza ricorrere agli oggetti. I tipi primitivi sono riconoscibili anche perché iniziano con una lettera minuscola (mentre i nomi delle classi iniziano sempre con una maiuscola). I tipi primitivi disponibili in Java sono i seguenti:
-
boolean
: valore booleano, può assumere i valoritrue
efalse
-
byte
: numero intero a 8 bit -
short
: numero intero a 16 bit -
int
: numero intero a 32 bit -
long
: numero intero a 64 bit -
float
: numero reale a 32 bit in virgola mobile (IEEE 754-1985) -
double
: numero reale a 64 bit in virgola mobile (IEEE 754-1985) -
char
: carattere unicode a 16 bit
Gli oggetti in Java sono istanze di una classe (dietro questa affermazione c’è un mondo, per spiegarla per bene non basta un post ma servirebbe un !). Una delle differenze importanti fra tipi primitivi e oggetti si ha se guardiamo la situazione della memoria a basso livello. Infatti nella locazione di memoria corrispondente a un tipo primitivo è presente il valore mentre in quella di un oggetto è presente il puntatore all’area di memoria che contiene l’oggetto. Un diagramma spiega meglio questa differenza:
Per arrivare alla situazione definita in questo diagramma basta creare due nuovi interi e due nuovi oggetti (creando nuove istanze della classe Persona
che contiene i campi nome
e cognome
):
int i1 = 5; int i2 = 7; Persona p1 = new Persona("Mario", "Rossi"); Persona p2 = new Persona("Mario", "Verdi");
Fin qui sembra tutto semplice, andiamo avanti facendo un po’ di assegnazioni:
i2 = i1; p2 = p1;
A questo punto quale è la situazione? Ovviamente i1
e i2
assumo lo stesso valore, stessa cosa anche per p1
e p2
. Ma se andiamo a vedere cosa è successo in memoria la situazione è un po’ più complicata:
Adesso p1
e p2
puntano allo stesso oggetto: abbiamo una condivisione di memoria che può risultare pericolosa se non gestita adeguatamente. Infatti richiamando un metodo setter su uno dei due oggetti (per esempio p1.setNome("Fabio")
) si modificherà l’oggetto condiviso dai due puntatori. Non è sicuramente un dramma (e a volte può essere una cosa utile da sfruttare) ma è comunque una cosa da tenere presente.
Equals Vs ==
Avendo chiara la differenza fra tipi primitivi e oggetti e il fatto che le variabili corrispondenti agli oggetti sono dei puntatori, i concetti alla base di equals
e ==
diventano semplici da capire. Iniziamo con l’operatore ==
: invocando questo costrutto viene confrontato il valore contenuto nella variabile (per capirsi quello scritto nei rettangoli sulla sinistra nei precedenti diagrammi). Quindi, nel caso di tipi primitivi viene confrontato il valore vero, mentre nel caso di oggetti viene confrontato l’indirizzo di memoria a cui i puntatori fanno riferimento.
Vediamo un esempio concreto. Il seguente codice stampa sulla console 4 volte il valore true
:
int i1 = 5; int i2 = 7; System.out.println(i1 != i2); i2 = i1; System.out.println(i1 == i2); Persona p1 = new Persona("Mario", "Rossi"); Persona p2 = new Persona("Mario", "Verdi"); System.out.println(p1 != p2); p2 = p1; System.out.println(p1 == p2);
Infatti come abbiamo visto nel paragrafo precedente p1
e p2
puntano allo stesso oggetto e quindi confrontando l’indirizzo di memoria corrispondente al puntatore si ha un esito positivo.
Vediamo un altro esempio leggermente più complicato. Definiamo due volte lo stesso valore/oggetto e confrontiamolo con ==
:
int i1 = 5; int i2 = 5; System.out.println(i1 == i2); Persona p1 = new Persona("Mario", "Rossi"); Persona p2 = new Persona("Mario", "Rossi"); System.out.println(p1 == p2);
In questo caso la prima condizione sui tipi primitivi è vera mentre quella sugli oggetti è falsa! Il motivo è chiaro, le variabili p1
e p2
in questo caso puntano a locazioni di memoria diverse quindi confrontando gli indirizzi di memoria si avrà un risultato negativo. Per ovviare a questo problema è necessario usare il metodo equals
che, a differenza dell’operatore ==
, confronta gli oggetti puntati entrando quindi in merito al contenuto dell’oggetto. Il metodo equals
è definito nella classe Object
quindi è disponibile su tutti gli oggetti (ma non sui tipi primitivi). Riproviamo quindi a eseguire lo stesso codice usando il metodo equals
per confrontare i due oggetti:
Persona p1 = new Persona("Mario", "Rossi"); Persona p2 = new Persona("Mario", "Rossi"); System.out.println(p1.equals(p2));
Eseguendo questo codice viene stampato sulla console ancora false
! 🙁 Come mai? Il motivo è che il metodo equals
è definito dentro la classe Object
con la seguente implementazione:
public boolean equals(Object obj) { return this == obj; }
L’implementazione di Object
del metodo equals
utilizza l’operatore ==
! Questa scelta può sembrare strana ma è l’unica che poteva essere fatta. infatti la classe Object
è la classe padre di tutte le classi Java, non è possibile dentro questa classe sapere come confrontare qualunque oggetto Java! Avrebbero potuto confrontare tutti i campi della classe usando la reflection ma l’implementazione sarebbe stata più complicata e molto meno performante (e comunque non sarebbe stata sempre corretta).
Il modo migliore per far funzionare l’esempio precedente è riscrivere il metodo equals
nella nostra classe Persona
per confrontare i campi nome
e cognome
. La strada più semplice è quella di far generare il codice ad Eclipse usando il comando Generate hashCode() and equals del menu source:
public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Persona other = (Persona) obj; if (cognome == null) { if (other.cognome != null) { return false; } } else if (!cognome.equals(other.cognome)) { return false; } if (nome == null) { if (other.nome != null) { return false; } } else if (!nome.equals(other.nome)) { return false; } return true; }
Il metodo hashCode
è fortemente legato al metodo equals
e viene utilizzato nel caso di collection basate su hash table (per esempio HashMap
e HashSet
), potete trovare maggiori informazioni sull’argomento nel nostro .
Occhio ai wrapper!
In Java oltre ai tipi primitivi esistono anche delle classi wrapper che corrispondono ai vari tipi primitivi. Al tipo primitivo int
corrisponde la classe wrapper Integer
, a float
corrisponde Float
, a double
la classe Double
e così via. Le istanze delle classi wrapper sono oggetti come gli altri: possono contenere metodi e le variabili di questo tipo possono assumere anche il valore null
. Queste classi sono state create per essere utilizzate con le Java Collection in quanto una collection può contenere solo oggetti e non tipi primitivi (maggiori informazioni sono disponibili nel ).
Abbiamo detto che le classi wrapper sono oggetti, per questo motivo è necessario utilizzare il metodo equals
per confrontare due istanze di una classe wrapper anche se logicamente sono molto simili ai tipi primitivi. Il seguente codice stampa quindi prima false
e poi true
:
Integer i1 = new Integer(5); Integer i2 = new Integer(5); System.out.println(i1 == i2); System.out.println(i1.equals(i2));
Le cose diventano un po’ strane se usiamo l’autoboxing (la feature disponibile a partire da Java 1.5 che permette di passare da tipi nativi a tipi wrapper senza usare una conversione esplicita). Il seguente codice è valido a partire da Java 1.5:
Integer i1 = 5; Integer i2 = 5; System.out.println(i1 == i2); System.out.println(i1.equals(i2));
Eseguendo questo codice viene stampato due volte il valore true
! Quale è il motivo per cui funziona correttamente anche l’operatore ==
creando due oggetti Integer
in questo modo? Il motivo è semplice: la prima riga corrisponde a Integer i1 = Integer.valueOf(5);
, il metodo valueOf
per evitare di creare molti oggetti mantiene una cache dei primi 127 numeri interi. Per questo invocando più volte il metodo valueOf
passando argomenti minori di 128 viene ritornato sempre lo stesso oggetto (e quindi anche l’operatore ==
funziona correttamente). Ovviamente non è prudente sfruttare questa cosa, sugli oggetti wrapper è sempre bene usare il metodo equals
oppure l’operatore ==
dopo aver estratto il valore primitivo corrispondente:
Integer i1 = 128; Integer i2 = 128; System.out.println(i1 == i2); System.out.println(i1.equals(i2)); System.out.println(i1.intValue() == i2.intValue());
Uguaglianza fra Enum
Un altro caso un po’ particolare è quello delle enum, costrutto Java disponibile a partire da Java 1.5. Una enum è a tutti gli effetti un oggetto Java ma per come è gestita c’è una sola istanza per ogni valore dell’enum stessa. Per questo motivo è possibile usare tranquillamente l’operatore ==
in quanto siamo sicuri che ogni volta che facciamo riferimento allo stesso valore stiamo manipolando la stessa istanza dell’oggetto enum.
Conclusioni
In questo post abbiamo parlato abbastanza a fondo del metodo equals
e abbiamo visto che niente è semplice come può sembrare! Se volete l’argomento potete dare un’occhiata ad di Joshua Bloch (disponibile anche nella ). Leggendo questo libro scoprirete cose abbastanza sorprendenti, per esempio che non è possibile definire il metodo equals
in modo corretto quando si ha a che fare con una gerarchia di classi!