Come creare una classe immutabile in Java

Introduzione

Questo articolo fornisce una panoramica su come creare una classe immutabile nella programmazione Java.

Un oggetto è immobile quando il suo stato non cambia dopo essere stato inizializzato. Ad esempio, String è una classe immutabile e, una volta istanziato, il valore di un oggetto String non cambia mai. Scopri di più su perché la classe String è immutabile in Java.

Poiché un oggetto immutabile non può essere aggiornato, i programmi devono creare un nuovo oggetto per ogni cambio di stato. Tuttavia, gli oggetti immutabili hanno anche i seguenti vantaggi:

  • Una classe immutabile è utile per scopi di caching perché non è necessario preoccuparsi dei cambiamenti di valore.
  • Una classe immutabile è intrinsecamente thread-safe, quindi non è necessario preoccuparsi della sicurezza dei thread in ambienti multithreaded.

Scopri di più su multithreading in Java e sfoglia le Domande di Intervista sul Multithreading in Java.

Creazione di una classe immutabile in Java

Per creare una classe immutabile in Java, è necessario seguire questi principi generali:

  1. Dichiarare la classe come final in modo che non possa essere estesa.
  2. Rendere tutti i campi private in modo che l’accesso diretto non sia consentito.
  3. Non fornire metodi setter per le variabili.
  4. Rendere tutti i campi mutabili final in modo che il valore di un campo possa essere assegnato solo una volta.
  5. Inizializzare tutti i campi utilizzando un metodo costruttore che esegue la copia profonda.
  6. Eseguire clonazione degli oggetti nei metodi getter per restituire una copia anziché restituire il riferimento all’oggetto effettivo.

La seguente classe è un esempio che illustra i concetti base dell’immutabilità. La classe FinalClassExample definisce i campi e fornisce il metodo costruttore che utilizza la copia profonda per inizializzare l’oggetto. Il codice nel metodo main del file FinalClassExample.java testa l’immutabilità dell’oggetto.

Creare un nuovo file chiamato FinalClassExample.java e copiare il seguente codice:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// campi della classe FinalClassExample
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Funzione getter per oggetti mutabili

	public HashMap<String, String> getTestMap() {
		return (HashMap<String, String>) testMap.clone();
	}

	// Metodo costruttore che esegue una copia profonda
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// La parola chiave "this" si riferisce all'oggetto corrente
		this.id=i;
		this.name=n;

		HashMap<String,String> tempMap=new HashMap<String,String>();
		String key;
		Iterator<String> it = hm.keySet().iterator();
		while(it.hasNext()){
			key=it.next();
			tempMap.put(key, hm.get(key));
		}
		this.testMap=tempMap;
	}

	// Test della classe immutabile

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// stampa i valori ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// cambia i valori delle variabili locali
		i=20;
		s="modified";
		h1.put("3", "third");
		// stampa nuovamente i valori
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

Compila ed esegui il programma:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Nota: Potresti ricevere il seguente messaggio durante la compilazione del file: Nota: FinalClassExample.java utilizza operazioni non controllate o non sicure perché il metodo getter sta utilizzando un cast non controllato da HashMap<String,String> a Oggetto. Puoi ignorare l’avviso del compilatore ai fini di questo esempio.

Ottieni il seguente output:

Output
Performing Deep Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second} ce testMap after changing variable from getter methods: {1=first, 2=second}

L’output mostra che i valori di HashMap non sono cambiati perché il costruttore utilizza una copia profonda e la funzione getter restituisce un clone dell’oggetto originale.

Cosa succede quando non si utilizza la copia profonda e il clonaggio

Puoi apportare modifiche al file FinalClassExample.java per mostrare cosa succede quando si utilizza la copia superficiale invece della copia profonda e restituire l’oggetto invece di una copia. L’oggetto non è più immutabile e può essere modificato. Apporta le seguenti modifiche al file di esempio (o copia e incolla dall’esempio di codice):

  • Elimina il metodo costruttore che fornisce la copia profonda e aggiungi il metodo costruttore che fornisce la copia superficiale evidenziato nel seguente esempio.
  • Nella funzione getter, elimina return (HashMap<String, String>) testMap.clone(); e aggiungi return testMap;.

Il file di esempio dovrebbe ora apparire così:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// campi della classe FinalClassExample
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Funzione getter per oggetti mutabili

	public HashMap<String, String> getTestMap() {
		return testMap;
	}

	// Metodo costruttore che esegue la copia superficiale

	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Shallow Copy for Object initialization");
		this.id=i;
		this.name=n;
		this.testMap=hm;
	}

	// Testa la classe immutabile

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// stampa i valori di ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// cambia i valori delle variabili locali
		i=20;
		s="modified";
		h1.put("3", "third");
		// stampa nuovamente i valori
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

Compila ed esegui il programma:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Ecco l’output che hai ottenuto:

Output
Performing Shallow Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second, 3=third} ce testMap after changing variable from getter methods: {1=first, 2=second, 3=third, 4=new}

L’output mostra che i valori della HashMap sono stati modificati perché il metodo del costruttore utilizza una copia superficiale, creando un riferimento diretto all’oggetto originale nella funzione getter.

Conclusione

Hai appreso alcuni dei principi generali da seguire nella creazione di classi immutabili in Java, compresa l’importanza della copia profonda. Continua il tuo apprendimento con altri tutorial Java.

Source:
https://www.digitalocean.com/community/tutorials/how-to-create-immutable-class-in-java