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 è un B e si sceglie equals(B), in stampaUguali è un Object e si sceglie equals(Object);
  • in esecuzione il late binding sceglie la versione di quella segnatura in base al tipo dinamico. B non ridefinisce equals(Object), quindi parte quella di Object, 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 x+yx+y, che vale 33 sia per (1,2)(1,2) sia per (2,1)(2,1): qui (1,2)(1,2) dà 1+37⋅2=751+37\cdot 2=75 e (2,1)(2,1) dà 2+37⋅1=392+37\cdot 1=39. 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 implementa super.clone() lancia CloneNotSupportedException, un'eccezione controllata che va gestita con try e catch.
  • public: rende clone disponibile ai clienti. Una ridefinizione può allargare l'accesso, non restringerlo.
  • super.clone(): è il clone di Object. 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 un Object, e il cast a B permette 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()

o1 : Aw : int[]o2 : Avv
Originale e copia condividono l'array: una modifica attraverso l'uno si vede nell'altro. Per questo il campo va copiato in profondità.

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 al clone di Object, 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 invoca clone, che è un D.
  • D è clonabile senza dichiarare implements Cloneable: eredita le interfacce implementate dalla base.
  • Non serve try e catch: l'eccezione è già gestita da super.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?

OggettoContaequals e hashCodeclone
Entità, mutabileidentitàquelli di Objectnon si ridefinisce
Valore, immutabilecontenutosi ridefiniscononon serve: si condivide il riferimento
Valore complesso, mutabilecontenutosi ridefinisconocopia 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 Cloneable
  • public 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