Lezione 10 · Canale 2 · mercoledì 14 ottobre 2026
Uguaglianza, copia e mutabilità degli oggetti
Progettazione del Software
Ridefinire equals con @Override
equals, ereditato da Object, se non è ridefinito si comporta come == e verifica l'uguaglianza superficiale. Per l'uguaglianza profonda si ridefinisce con uno schema in cinque passi: firma giusta, argomento non null, stessa classe, cast, confronto dei campi.
public class B {
private int x, y;
@Override
public boolean equals(Object o) {
if (o != null && getClass().equals(o.getClass())) {
B b = (B) o;
return (x == b.x) && (y == b.y);
}
else return false;
}
}
@Override chiede al compilatore di controllare che il metodo ridefinisca davvero un metodo ereditato. Si mette sempre sopra un metodo ridefinito, perché sbagliare la firma è facile e il codice compila lo stesso.
Overriding, non overloading
Con parametro di tipo B al posto di Object, equals(B b) non ridefinisce equals(Object): è un overloading, e B ha due metodi equals. Con b1 e b2 uguali campo per campo, b1.equals(b2) dà true, mentre lo stesso confronto dentro stampaUguali(Object o1, Object o2) dà false. La scelta del metodo avviene in due momenti:
- in compilazione il tipo statico dell'argomento sceglie la segnatura fra quelle in overloading: in
b1.equals(b2)l'argomento è unBe si sceglieequals(B), instampaUgualiè unObjecte si sceglieequals(Object); - in esecuzione il late binding sceglie la versione di quella segnatura in base al tipo dinamico.
Bnon ridefinisceequals(Object), quindi parte quella diObject, che confronta i riferimenti.
Con @Override su equals(B b) il compilatore segnala subito l'errore.
equals e hashCode
Ridefinendo equals si deve ridefinire anche hashCode, perché in Java vale un vincolo: se o1.equals(o2), allora o1.hashCode() == o2.hashCode().
hashCode restituisce un intero calcolato con gli stessi campi usati per l'uguaglianza profonda. Come in una biblioteca ordinata per scaffali, l'intero indica lo scaffale in cui cercare, poi equals trova l'oggetto preciso. Se tutti gli oggetti finiscono sullo stesso scaffale la ricerca rallenta: la funzione deve distribuirli.
@Override
public int hashCode() {
return x + 37 * y; // funzione degli stessi campi di equals
}
Moltiplicare per un numero primo distribuisce meglio della somma , che vale sia per sia per : qui dà e dà . In alternativa Objects.hash(x, y), del pacchetto java.util, combina i campi da sé.
equals e hashCode nelle classi derivate
Una classe derivata richiama prima equals della base, per riuso, e poi confronta i campi propri. Così non si duplica il confronto e si considerano anche i campi privati della base, che la derivata non legge.
public class D extends B {
private int z;
@Override
public boolean equals(Object o) {
if (super.equals(o)) {
D d = (D) o;
return z == d.z;
}
else return false;
}
@Override
public int hashCode() {
return super.hashCode() + 53 * z;
}
}
Il cast a D è sicuro: se super.equals(o) è vero, o non è null e ha la stessa classe di this, cioè D. Anche hashCode riusa quello della base e aggiunge il contributo di z: vale x + 37 * y + 53 * z.
Copia superficiale e copia profonda
Copiare un oggetto può voler dire due cose:
- copia superficiale: si copia l'identificatore dell'oggetto, cioè il riferimento;
- copia profonda: si crea un nuovo oggetto, clone dell'originale, identico ma con un identificatore diverso.
C c1 = new C(4, 5);
C c2;
c2 = c1; // copia superficiale
Dopo c2 = c1 ci sono due variabili e un solo oggetto: c1 e c2 sono uguali superficialmente, e una modifica fatta attraverso uno dei due riferimenti si vede anche attraverso l'altro. Con la copia profonda copia e originale restano indipendenti.
clone
protected Object clone() di Object serve per la copia profonda. Essendo protected, anche se tutti la ereditano non è accessibile ai clienti, a meno che la classe non la ridefinisca. Copiare non sempre ha senso, ma serve per provare un effetto senza toccare l'originale, per esempio la media dei voti con un voto in più.
public class B implements Cloneable {
private int x, y;
@Override
public Object clone() {
try {
B b = (B) super.clone();
// copia profonda dei campi dati,
// incluse liste, array, ecc.
return b;
} catch (CloneNotSupportedException e) {
throw new InternalError("...");
}
}
}
implements Cloneable:Cloneableè un'interfaccia vuota che marca come clonabili gli oggetti della classe. Su un oggetto che non la implementasuper.clone()lanciaCloneNotSupportedException, un'eccezione controllata che va gestita contryecatch.public: rendeclonedisponibile ai clienti. Una ridefinizione può allargare l'accesso, non restringerlo.super.clone(): è ilclonediObject. Crea un nuovo oggetto della stessa classe di quello su cui è invocato e ne copia i campi in modo superficiale, come una copia esatta della porzione di memoria dell'oggetto. Non c'è altro modo per ottenerla.- Il cast:
super.clone()restituisce unObject, e il cast aBpermette di operare sui campi.
Un campo che rappresenta una lista o un array contiene un riferimento, quindi dopo super.clone() è lo stesso nei due oggetti. In generale, per una classe con un campo v che riferisce un array:
Un campo riferimento dopo super.clone()
Dopo super.clone() si assegna al campo della copia un nuovo array con gli stessi elementi. Il cliente chiama clone in modo esplicito e converte il risultato con un cast:
B b1 = new B();
B b2 = (B) b1.clone();
clone nelle classi derivate
La derivata richiama prima il clone della base, per riuso, e poi copia in profondità i campi propri.
public class D extends B {
private int z;
@Override
public Object clone() {
D d = (D) super.clone();
// copia profonda dei campi propri di D
return d;
}
}
super.clone()è indispensabile: attraverso la catena di ereditarietà si deve arrivare alclonediObject, l'unico che crea un oggetto della classe giusta.- Il cast a
Dè sicuro: la copia ha la stessa classe dell'oggetto su cui si invocaclone, che è unD. Dè clonabile senza dichiarareimplements Cloneable: eredita le interfacce implementate dalla base.- Non serve
tryecatch: l'eccezione è già gestita dasuper.clone().
Oggetti mutabili e immutabili
Lo stato di un oggetto è l'insieme dei valori dei suoi campi in un dato momento.
- Oggetto mutabile: il suo stato può cambiare durante l'esecuzione. Ha metodi con side effect, che modificano lo stato, come i metodi
set. - Oggetto immutabile: il suo stato non può cambiare. I metodi leggono i campi, calcolano e restituiscono risultati; se serve un oggetto modificato, se ne crea uno nuovo.
String è immutabile: toUpperCase crea una nuova stringa, che si perde se non è assegnata.
String s = "ciao";
s.toUpperCase(); // s resta "ciao"
s = s.toUpperCase(); // ora s è "CIAO"
Gli oggetti immutabili rappresentano di solito valori, come gli attributi in UML. Quelli mutabili rappresentano entità, gli oggetti nel senso di UML: hanno identità propria e cambiano nel tempo restando se stessi, come un conto su cui si deposita denaro. Per efficienza rappresentano anche valori complessi, come le collezioni.
Cosa ridefinire
Dipende da due domande: conta l'identità o il contenuto? Può cambiare nel tempo?
| Oggetto | Conta | equals e hashCode | clone |
|---|---|---|---|
| Entità, mutabile | identità | quelli di Object | non si ridefinisce |
| Valore, immutabile | contenuto | si ridefiniscono | non serve: si condivide il riferimento |
| Valore complesso, mutabile | contenuto | si ridefiniscono | copia profonda, se si vuole permettere la copia |
L'uguaglianza di un'entità è quella dell'identificatore, che equals di Object verifica già. Ridefinirlo sui dati sbaglia: due studenti omonimi nati lo stesso giorno diventerebbero uguali, e in un insieme di iscritti il secondo verrebbe scartato. Una copia di una persona o di un conto sarebbe un duplicato che nel sistema non esiste, quindi clone resta quello di Object.
Per un valore complesso conta il contenuto: due insiemi con gli stessi numeri sono lo stesso insieme, qualunque sia l'ordine di inserimento.
Formulario
equals
- firma
equals(Object o) - sempre
@Override - derivata:
super.equals(o)
hashCode
- uguali: stesso codice hash
- stessi campi di
equals - derivata:
super.hashCode() - più il contributo di
z
clone
implements Cloneablepublic Object clone()super.clone(): superficiale- copiare i campi riferimento
- cast e
try/catch - derivata: niente
try/catch
Mutabilità
- immutabile: stato fisso
- mutabile: side effect
Stringè immutabile
Cosa ridefinire
- entità: nulla
- valore:
equals,hashCode - collezione: anche
clone, se si permette la copia