mer 09 ottobre 2024 - Lo Sviluppatore  anno VI

Strutture Immutabili in Java

Condividi

In questo articolo cerchiamo di chiarire il concetto di immutabilità di una oggetto in Java. Spesso i programmatori credono che basti dichiarare un oggetto final per renderlo immutabile ma, anche se questa è una condizione necessaria, non è sufficiente per definire un oggetto immutabile. Prima di dare delle regole su come creare classi immutabili vediamo di definire questi tipi di classi e i vantaggi che abbiamo nel loro utilizzo.

Cosa è un oggetto immutabile?

Un oggetto si dice immutabile,  quando una volta creato e inizializzato, non può essere più modificato. Possiamo chiamare metodi di lettura (ad esempio metodi getter), copiare gli oggetti o passarli come argomento ad altri metodi, ma nessun metodo dovrebbe consentire di modificare lo stato dell’oggetto. Le classi wrapper (come Integer e Float) e le classe String sono esempi noti di classi immutabili.

Consideriamo come esempio la classe String. Un oggetto di tipo String è immutabile in quanto una volta creato, non è possibile modificarlo. Prendiamo ad esempio il metodo trim() che rimuove eventuali spazi presenti all’inizio e la  fine della stringa. Questo metodo, come anche gli altri disponibili nella classe String, non modificano l’oggetto a cui vengono applicati ma agiscono su una copia dell’oggetto originario, restituendo appunto un nuovo oggetto con il contenuto modificato. Per restare sempre nel campo delle stringhe di controparte un oggetto di tipo StringBuilder invece è mutabile in quanto i metodi agiscono sull’oggetto originario.

Strutture Immutabili in Java 1

 

 

Benefici nell’uso di strutture immutabili

Elenchiamo  e poi dettagliamo di seguito i principali motivi nell’usare strutture immutabili:

  • Assenza di stati non validi
  • Thread safety
  • Codice più facile da capire
  • Codice più facile da testare
  • Possono essere usati come value types

Assenza di stati non validi

Quando un oggetto è immutabile, è difficile avere l’oggetto in uno stato non valido. L’oggetto può essere istanziato solo attraverso il suo costruttore, in questo modo, i parametri richiesti nel costruttore per mantenere uno stato valido possono essere imposti. Vediamo un esempio:

/** la classe Luogo ha tre campi indirizzo, città e stato. Se forniamo un costruttore senza parametri 
e i metodi setter l'oggetto può avere uno stato non valido */
Luogo l = new Luogo();
l.setIndirizzo("Via Verdi 15"); // il campi citta e stato non vengono inizializzati

/** Se invece forniamo solo il seguente costruttore e nessun metodo setter l'oggetto 
sarà sempre in uno stato valido */
Luogo l = new Luogo("Via Verdi 15", "Roma", "Italia");

Thread safety

Poiché l’oggetto non può essere modificato, può essere condiviso tra thread senza problemi di mutua esclusione o problemi di mutazione dei dati.

Codice più facile da capire

Riprendendo l’esempio precedente vediamo che in generale è più facile usare un costruttore per inizializzare l’oggetto anzichè usare i metodi setter, questo perchè il costruttore impone gli argomenti richiesti mentre l’uso dei metodi setter non può essere imposto al momento della compilazione.

Codice più facile da testare

Poiché gli oggetti sono più prevedibili, non è necessario testare tutte le permutazioni dei metodi di inizializzazione; e anche altre parti di codice che utilizzano queste classi diventano più prevedibili, con meno possibilità di NullPointerExceptions.

Possono essere usati come value types

Immagina un importo in denaro, diciamo 10 euro. 10 euro saranno sempre 10 euro. Tradotto in codice, questo potrebbe apparire come  public Money(final BigInteger importo, final Currency valuta). Come puoi vedere in questo codice, non è possibile cambiare il valore di 10 euro in qualcosa di diverso da questo, e quindi quanto sopra può essere usato in sicurezza come value type.

I campi final non rendono gli oggetti immutabili

Come accennato in precedenza, alcuni sviluppatori credono che basti dichiarare i campi di una classe final per rendere gli oggetti immutabili. Sfortunatamente, non è così semplice, e cerchiamo di capire il perchè vedendo un esempio:

Il seguente codice NON rende l’oggetto immutabile:

final Persona persona = new Persona("Marco");
Perchè no? Bene, mentre persona è un campo final, e non può essere riassegnato, la classe Persona potrebbe avere un metodo setter o altri metodi mutator, che ne possono cambiare lo stato, per esempio:
persona.setNome("Luigi");
 o ancora tale classe potrebbe esporre una lista di oggetti di tipo Luogo e sarebbe possibile fare una cosa del genere e mutare l’oggetto:
persona.getIndirizzi().add(new Luogo("Via Verdi 15", "Roma", "Italia"));

Abbiamo visto quindi come il solo uso della keyword final non rende un oggetto immutabile. Ma quindi, come si modella una classe affinchè sia immutabile? Vediamo di seguito le linee guida per fare ciò

Definire classi immutabili

  • Dichiarare tutti campi come private in modo da impedirne l’accesso diretto
  • Dichiarare i campi della classe come final ed inizializzali nel costruttore. Per i tipi primitivi e classi immutabili è sufficiente definirli come final, affinchè non sia più possibile modificarli, una volta inizializzati. Per i campi di tipi mutabili vanno presi ulteriori accorgimenti oltre all’uso della parola final. perchè l’oggetto potrebbe esporre metodi che modificano lo stato interno. In questi casi:
    • Assicurarsi che i metodi non cambino il contenuto all’interno di quegli oggetti mutabili.
    • Non condividere i riferimenti al di fuori delle classi, ad esempio come valore di ritorno dai metodi di quella classe. Se i riferimenti ai campi che sono mutabili sono accessibili dal codice al di fuori della classe, possono finire per modificare il contenuto dell’oggetto. Per evitare questo, inizializzare i campi nel costruttore usando una copia dell’oggetto in input (deep copy).
    • Se c’è la necessità di restituire un riferimento, restituire una copia dell’oggetto (in modo che i contenuti originali rimangano intatti anche se il contenuto all’interno dell’oggetto restituito viene modificato).
  • Fornire solo metodi di accesso (getter methods) ma non metodi mutator (setter methods)
  • Dichiarare la classe come final affinchè non possa essere estesa. Perché? Se la classe è ereditabile, i metodi nella sua classe derivata possono essere sovrascritti e quindi potrebbero modificane i campi.

Alla luce di queste linee guida riprendiamo l’esempio della classe Persona visto sopra e vediamo come renderla immutabile:

public final class Persona {	// final class, affinchè non possa essere estesa
  // String è immutabile quindi basta l'uso di final per impedire la modifica del riferimento
  private final String nome;     
  private final List<Luogo> indirizzi;

  public Persona(String nome, List<Luogo> indirizzi) {
    this.nome = nome;
    // qui assegnamo una copia non modificabile della lista passata in input 
    // per prevenire modifiche esterne
    this.indirizzi =  Collections.unmodifiableList(new ArrayList<Luogo>(this.indirizzi));
  }

  public String getNome() {
    return this.nome;   // String è immutabile, può essere esposta
  }

  public List<Luogo> getIndirizzi() {
   // possiamo esporre questa lista in quanto è stata resa non modificabile nel costruttore
   return this.indirizzi; 
  }
}

public final class Luogo { // final class
 // qui i campi sono tutti di tipo String e quindi immutabili
 private final String indirizzo; 
 private final String citta; 
 private final String paese;

 public Luogo(String indirizzo, String citta, String paese) {
 this.indirizzo = indirizzo
 this.citta = citta;
 this.paese = paese;
 }

 public String getIndirizzo() {
 return indirizzo;
 }

 public String getCitta() {
 return citta;
 }

 public String getPaese() {
 return paese;
 }
}

Usiamo queste classi per creare un oggetto immutabile:

List<Luogo> luoghi = new ArrayList<Luogo>();
luoghi.add(new Luogo("Via Verdi 15", "Roma", "Italia"));
luoghi.add(new Luogo("Via Garibaldi 10", "Pisa", "Italia"));
final Person persona = new Person("Mario", luoghi);

 

L’oggetto persona è immutabile in quanto non può essere riassegnato, qualsiasi modifica alla lista luoghi, successiva alla creazione dell’oggetto persona non ha alcuna influenza sulla lista degli indirizzi presenti nell’oggetto.  La classe Persona non può essere estesa.

 

Lascia un commento

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.

Top