In questo post saranno esaminate le classi immutabili in Java e come queste siano ideali per scrivere codice più sicuro e performante. Saranno analizzati, inoltre, quali siano i vincoli da rispettare per essere sicuri di avere realizzato una classe immutabile.
Cosa sono le classi immutabili
Una classe è immutabile se lo stato di un oggetto di quella classe (detto anche oggetto immutabile) non può essere modificato dopo che è stato creato. Prima di entrare nel merito delle caratteristiche di una classe immutabile e quali siano i vantaggi di questo tipo di implementazione, è bene chiarire un aspetto. Non tutte le classi sono adatte a essere trasformate in immutabili, in particolar modo quelle più legate alla business logic e quindi più soggette a modifiche durante il ciclo di vita dell’applicazione.
Anche se il concetto di immutabile può risultare nuovo, in realtà chi programma in Java, anche solo a livello basilare, ha già utilizzato classi di questo tipo: infatti diverse classi di uso comune sono immutabili come java.lang.Long
, java.lang.String
e java.lang.Boolean
. La classe java.util.Date
che potrebbe essere ritenuta di questo tipo, in realtà è mutabile e questo viene addirittura ritenuto1 un punto debole del design della libreria.
Un esempio di classe mutabile
Come esempio viene utilizzata la classe Pizza
. Esaminando il codice si vede chiaramente che lo stato può essere variato dopo la costruzione dell’oggetto.
package it.cosenonjaviste.classiimmutabili.mutabile; import java.text.DecimalFormat; import java.util.EnumSet; import java.util.Set; public class Pizza { public enum Formato { NORMALE, BABY, MAXI, CALZONE; } public enum Ingrediente { PROSCIUTTO, FUNGHI, PATATE, SALSICCIA, CARCIOFI, SPECK; } protected Formato formato; protected Set ingredienti = EnumSet.noneOf(Ingrediente.class); public Formato getFormato() { return formato; } public void setFormato(Formato formato) { this.formato = formato; } public void aggiungiIngrediente(Ingrediente i) { ingredienti.add(i); } public void rimuoviIngrediente(Ingrediente i) { ingredienti.add(i); } public Set getIngredienti() { return ingredienti; } public double getPrezzo() { double prezzo = 0.0; switch (formato) { case BABY: prezzo += 2.0; break; case NORMALE: case CALZONE: prezzo += 4.0; break; case MAXI: prezzo += 7.0; break; } prezzo += ingredienti.size() * 1.0; return prezzo; } public String toString() { return formato + " " + ingredienti + " " + new DecimalFormat("#,##").format(getPrezzo()); } }
Di seguito un esempio di codice chiamante.
Pizza prosciutto = new Pizza(); Pizzeria.pizze.add(prosciutto); prosciutto.setFormato(Formato.CALZONE); prosciutto.aggiungiIngrediente(Ingrediente.PROSCIUTTO); System.out.println("Ho segnato l'ordine di " + prosciutto);
Vantaggi
Quali sono, dunque, i vantaggi nel rendere Pizza
immutabile? Qualsiasi oggetto (anche mutabile) che nel suo stato interno abbia un’instanza di Pizza
può esporla all’esterno senza effettuare prima una copia di sicurezza. Infatti, nessun altro oggetto può modificare lo stato di un oggetto di tipo Pizza
e, in particolare, nessun thread può corrompere lo stato interno di una classe immutabile. Per tecnica di implementazione, le classi immutabili risultano di conseguenza thread-safe e consentono di evitare metodi synchronized
.
Gli oggetti immutabili possono essere dunque condivisi senza problemi tra oggetti diversi e questo può consentire un risparmio nella memoria occupata. Può essere buona norma se la classe immutabile fornisce già alcune istanze più comuni o con particolare significato come ad esempio Long.MIN_VALUE
o Boolean.TRUE
.
Gli oggetti immutabili sono anche ottime chiavi da utilizzare in un HashSet
o in un HashMap
. Gli oggetti mutabili possono variare il loro hashCode
in base allo stato corrente e, come abbiamo visto nel post sulle HashMap, se questo si verifica l’oggetto sarà ancora presente nella collezione, ma non più accessibile per chiave utilizzando contains()
.
Non è da trascurare, inoltre, il fatto che le classi immutabili siano più semplici da programmare poiché le loro istanze possono assumere uno e un solo stato che viene mantenuto dalla costruzione in poi. Garantendo che tutti i costruttori rispettino determinate condizioni, diminuisce lo sforzo necessario per il test poiché lo stato interno è sempre noto e soddisfa sempre determinate asserzioni. Questo aspetto le differenzia dalla classi mutabili dove le possibili combinazioni dello stato interno sono pressoché infinite.
Come conseguenza dell’invarianza dello stato interno, infine, il valore di qualsiasi metodo che è funzione esclusivamente dello stato (come ad esempio hashCode()
o toString()
) può essere calcolato una volta sola e memorizzato internamente per poi venir restituito senza ripetere il calcolo.
Come rendere immutabile una classe
Rendere Pizza
immutabile è un’operazione quasi meccanica assicurandosi di seguire le seguenti regole:
- rendere tutti i campi
final
; - rendere la classe
final
o comunque impedire l’estensione al di fuori del package di appartenenza (per mantenere un certo grado di flessibilità); - eliminare tutti i metodi che modifichino lo stato interno dell’oggetto (detti mutatori);
- ogni variabile membro che fa riferimento a oggetti mutabili (come ad esempio array, collezioni o la classe
java.util.Date
):- va resa privata;
- non deve mai essere restituita all’esterno se non attraverso una sua copia;
- deve avere reference solo all’interno della classe; quindi nel caso in cui la variabile membro sia inizializzata nel costruttore con un oggetto mutabile, va memorizza una copia dell’oggetto al posto dell’oggetto originale;
- non deve essere variata dopo la costruzione dell’oggetto (si vedrà una possibile eccezione a questa regola nel codice di
Pizza
).
In pratica, per rendere immutabile una classe, è indispensabile rimuovere gli elementi che consentono di modificare lo stato dall’esterno, come mutatori, classi derivate, o semplicemente campi pubblici. Inoltre, quando la classe memorizza un reference ad un oggetto mutabile, come ad esempio un ArrayList
, le regole precedenti non sono sufficienti ma è necessario anche che lo stato dell’oggetto mutabile non vari. Il modo più sicuro per farlo è realizzare una copia dell’oggetto quando questo viene passato dall’esterno e memorizzato nello stato interno o viceversa. Infatti solo il reference è final
, nulla garantisce che l’oggetto mutabile non sia modificato dal codice chiamante dopo che l’oggetto immutabile è stato costruito.
La classe di esempio in versione immutabile
Qui sotto è presentata la classe Pizza
immutabile: si noti che oltre ad applicare le regole indicate, sono state aggiunte alcune istanze utilizzate spesso e che le variabili membro prezzo
e toString
sono inizializzate in modo lazy. Quest’ultimo dettaglio costituisce un’eccezione alle regole precedenti in nome dell’efficienza. Tuttavia non viola l’immutabilità della classe, perché nessun metodo produce una modifica dello stato visibile esternamente dal codice chiamante
public final class Pizza { public static final Pizza MARGHERITA = new Pizza(Formato.NORMALE, EnumSet.noneOf(Ingrediente.class)); public static final Pizza PROSCIUTTO = new Pizza(Formato.NORMALE, EnumSet.of(Ingrediente.PROSCIUTTO)); public static final Pizza CALZONE_PROSCIUTTO = new Pizza(Formato.CALZONE, EnumSet.of(Ingrediente.PROSCIUTTO)); public enum Formato { NORMALE, BABY, MAXI, CALZONE } public enum Ingrediente { PROSCIUTTO, FUNGHI, PATATE, SALSICCIA, CARCIOFI, SPECK } private final Formato formato; private final Setingredienti; private Double prezzo; private String toString; public Formato getFormato() { return formato; } public Set getIngredienti() { return EnumSet.copyOf(ingredienti); } public Pizza(Formato formato, Set ingredienti) { this.formato = formato; this.ingredienti = EnumSet.copyOf(ingredienti); } public double getPrezzo() { if (prezzo == null) { prezzo = 0.0; switch (formato) { case BABY: prezzo += 2.0; break; case NORMALE: case CALZONE: prezzo += 4.0; break; case MAXI: prezzo += 7.0; break; } prezzo += ingredienti.size() * 1.0; } return prezzo; } public String toString() { if (toString == null) { toString = formato + " " + ingredienti + " " + new DecimalFormat("#,##").format(getPrezzo()); } return toString; }
Svantaggi e possibili rimedi
Chiaramente le classi immutabili hanno anche degli svantaggi. Dato che un oggetto immutabile non può essere variato, se si vuole effettuare una modifica sarà necessario costruire un nuovo oggetto che differisce dal primo solo per la variazione voluta. Questo aspetto può risultare critico quando la variazione avviene in un ciclo. Viene ripristinato il metodo aggiungiIngrediente()
e creata una pizza con tutti gli ingredienti.
public Pizza aggiungiIngrediente(Ingrediente i) { EnumSetnuoviIngredienti = EnumSet.copyOf(this.ingredienti); nuoviIngredienti.add(i); return new Pizza(this.formato, nuoviIngredienti); }
Pizza tutto = Pizza.MARGHERITA; for (Ingrediente i : Ingrediente.values()) tutto = tutto.aggiungiIngrediente(i);
All’interno del ciclo viene creata un’istanza (immutabile) di Pizza
per ogni ingrediente. Questo è chiaramente un spreco computazionale poiché l’unica istanza che interessa è quella generata nell’ultimo ciclo ma si è costretti a generarle tutte per arrivare al risultato finale.
Per rimediare a questi problemi di efficienza, la soluzione è quella di dotare la classe di una classe mutabile compagna che in pratica non è altro che un builder della classe immutabile. Il builder viene utilizzato per parametrizzare l’oggetto immutabile prima che questo sia creato e al momento della creazione viene restituito un’oggetto immutabile che ha lo stesso stato di quello mutabile. Per brevità di esposizione, non verrà dato il codice del possibile builder di Pizza
, tuttavia per comprendere la relazione esistente basta pensare, ad esempio, all’interazione tra String
(immutabile) e StringBuilder
(mutabile).
Conclusioni
Una classe immutabile è semplice da realizzare e permette di semplificare il codice e il test oltre a ridurre la memoria occupata dall’applicazione. In generale tutte le classi che modellano un value object come Integer
o String
sono ottime candidate a diventare immutabili. Meno adatti sono invece i business object e tutti quegli oggetti hanno attributi variabili nel tempo. A fronte di una semplificazione del codice, le classi immutabili possono avere problemi di efficienza in alcuni specifici casi. A questa si pone rimedio dotando la classe di una compagna mutabile da utilizzare fino a che non si vuole ottenere un oggetto immutabile con lo stato corrente della compagna.
Riferimenti
Libri
Articoli
- Java theory and practice: To mutate or not to mutate?. Brian Goetz.
- Immutable classes. Domingos Neto.
1Java Concurrency in Practice. Brian Goetz , Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea