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.
Indice
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.
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");
persona.setNome("Luigi");
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.