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.