Le classi immutabili in Java

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 Set<Ingrediente> ingredienti;
	private Double prezzo;
	private String toString;

	public Formato getFormato() {
		return formato;
	}

	public Set<Ingrediente> getIngredienti() {
		return EnumSet.copyOf(ingredienti);
	}

	public Pizza(Formato formato, Set<Ingrediente> 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) {
		EnumSet<Ingrediente> nuoviIngredienti = 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

1Java Concurrency in Practice. Brian Goetz , Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Databiz Srl. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub

  • Nazzareno Sileno

    Perchè non usare un FlyWeight invece che un oggetto immutabile?

  • Giampaolo Trapasso

    Grazie innanzitutto del commento perché è spunto di riflessione. 
    Obiettivo del post è stato parlare delle classi immutabili, su cosa sono e che vantaggi possono dare. Flyweight e classi immutabili sono argomenti correlati. Quando si vuole implementare il Flyweight (tipicamente per ridurre il numero di oggetti istanziati di una classe), sicuramente si analizza lo stato degli oggetti e si divide la parte mutabile da quella immutabile. Quella immutabile può essere riutilizzata e condivisa, realizzando quindi il pattern. 

    Per non rendere troppo lungo l’articolo, mi sono concentrato solo su ciò che rende immutabile una classe e i pro che ne seguono anche se non si applica quel pattern; è mia intenzione però scrivere un altro post sul Flyweight partendo da dove mi sono fermato su questo.