Vediamo in questo articolo come creare una classe immutabile in java. Ma cosa è un oggetto immutabile? Un oggetto immutabile è un oggetto che una volta istanziato e inizializzato non cambia mai il suo stato. Per esempio la classe String è un esempio di classe immutabile perchè una volta istanziata il suo valore non cambia.
Le classi immutabili sono utili ad esempio per essere inseriti in una cache in quanto non è necessario preoccuparsi delle modifiche del valore.
Un altro vantaggio delle classi immutabili è che sono intrinsecamente thread-safe, quindi non è necessario preoccuparsi della sicurezza dei thread in caso di ambiente multi-thread.
Per creare una classe immutabile occorre seguire i seguenti passi:
- Dichiarare la classe come final in modo che non possa essere estesa oppure rendere private il costruttore ed usare un factory method (metodo statico) per generare le istanze.
- Rendi private tutti i campi in modo che l’accesso diretto non sia consentito.
- Non fornire metodi setter per le variabili
- Rendi tutti i campi final in modo che il valore possa essere assegnato solo una volta.
- Inizializza tutti i campi tramite un costruttore che esegue la copia profonda (deep copy).
- Nei metodi getter di campi di tipi mutabili restituire una copia piuttosto che restituire il riferimento all’oggetto reale.
Esempio
Ma passiamo ad un esempio che è sempre il miglior modo per comprendere bene le cose.
package it.losviluppatore.esempi; import java.util.HashMap; import java.util.Iterator; public final class ClasseImmutabile { private final Integer id; private final String nome; private final HashMap<String,String> testMap; // public ClasseImmutabile(int id, String nome, HashMap<String, String> testMap) { // System.out.println("Inizializzazione diretta"); // this.id = id; // this.nome = nome; // this.testMap = testMap; // } public ClasseImmutabile(int id, String nome, HashMap<String, String> inputMap) { System.out.println("Inizializzazione mediante Deep Copy"); // normale init dei campi immutabili this.id = id; this.nome = nome; // init del capo di tipo mutabile (HashMap) mediante copia delle coppie chiave-valore estratti dalla mappa in input HashMap<String,String> tempMap = new HashMap<String,String>(); String key; Iterator<String> it = inputMap.keySet().iterator(); while(it.hasNext()){ key=it.next(); tempMap.put(key, inputMap.get(key)); } this.testMap=tempMap; } public Integer getId() { return id; } public String getNome() { return nome; } // Si restituisce una copia della HashMap in modo tale l'oggetto originale non possa essere modificato public HashMap<String, String> getTestMap() { return (HashMap<String, String>) testMap.clone(); // return testMap; } public static void main(String[] args) { Integer i = 10; String str = "originale"; HashMap<String,String> inputMap = new HashMap<String,String>(); inputMap.put("1", "Uno"); inputMap.put("2", "Due"); System.out.println("Variabili locali passate in input: id="+i+", str="+str+", inputMap="+inputMap); ClasseImmutabile cx = new ClasseImmutabile(i, str, inputMap); System.out.println("Init oggetto"); System.out.println("Output:"); System.out.println(" cx.getId() = "+cx.getId()); System.out.println(" cx.getNome() = "+cx.getNome()); System.out.println(" cx.getTestMap() = "+cx.getTestMap()); i = 20; str = "modificata"; inputMap.put("3", "Tre"); System.out.println("Modifico i valori delle variabili locali passate in input: id="+i+", str="+str+", inputMap="+inputMap); System.out.println("Output:"); System.out.println(" cx.getId() = "+cx.getId()); System.out.println(" cx.getNome() = "+cx.getNome()); System.out.println(" cx.getTestMap() = "+cx.getTestMap()); } }
Eseguiamo la prima versione usando il costruttore che esegue la deep copy e il getter dell’oggetto HashMap che restituisce una copia del campo. Di seguito l’output:
Variabili locali passate in input: id=10, str=originale, inputMap={1=Uno, 2=Due} Inizializzazione mediante Deep Copy Init oggetto Output: cx.getId() = 10 cx.getNome() = originale cx.getTestMap(): = {1=Uno, 2=Due} Modifico i valori delle variabili locali passate in input: id=20, str=modificata, inputMap={1=Uno, 2=Due, 3=Tre} Output: cx.getId() = 10 cx.getNome() = originale cx.getTestMap() = {1=Uno, 2=Due}
Come si vede i campi della nostra classe non hanno subito alcuna modifica.
Scommentiamo adesso il costruttore che inizializza i campi direttamente e il getTestMap() che restituisce direttamente il campo testMap, commentiamo l’altro costruttore ed eseguiamo:
Variabili locali passate in input: id=10, str=originale, inputMap={1=Uno, 2=Due} Inizializzazione diretta Init oggetto Output: cx.getId() = 10 cx.getNome() = originale cx.getTestMap() = {1=Uno, 2=Due} Modifico i valori delle variabili locali passate in input: id=20, str=modificata, inputMap={1=Uno, 2=Due, 3=Tre} Output: cx.getId() = 10 cx.getNome() = originale cx.getTestMap() = {1=Uno, 2=Due, 3=Tre}
Come si può vedere i due campi di tipo immutabile (Integer e String) sono rimasti come prima mentre la variabile di tipo HashMap (classe mutabile) è cambiata in quanto riferisce direttamente la variabile passata in input. Questa seconda versione non rende la nostra classe immutabile.