Cómo crear una clase inmutable en Java

Introducción

Este artículo proporciona una visión general de cómo crear una clase inmutable en la programación Java.

Un objeto es inmutable cuando su estado no cambia después de haber sido inicializado. Por ejemplo, String es una clase inmutable y, una vez instanciado, el valor de un objeto String nunca cambia. Aprende más sobre por qué la clase String es inmutable en Java.

Como un objeto inmutable no puede ser actualizado, los programas necesitan crear un nuevo objeto para cada cambio de estado. Sin embargo, los objetos inmutables también tienen los siguientes beneficios:

  • Una clase inmutable es buena para propósitos de almacenamiento en caché porque no tienes que preocuparte por los cambios de valor.
  • Una clase inmutable es inherentemente segura para subprocesos, así que no tienes que preocuparte por la seguridad de los subprocesos en entornos multi-hilo.

Aprende más sobre la programación multi-hilo en Java y consulta las Preguntas de entrevista sobre Multi-Hilo en Java.

Crear una Clase Inmutable en Java

Para crear una clase inmutable en Java, es necesario seguir estos principios generales:

  1. Declarar la clase como final para que no pueda ser extendida.
  2. Hacer que todos los campos sean private para que no se permita el acceso directo.
  3. No proporcionar métodos setter para las variables.
  4. Hacer que todos los campos mutables sean final para que el valor de un campo solo pueda ser asignado una vez.
  5. Inicializar todos los campos usando un método constructor que realice una copia profunda.
  6. Realizar clonación de objetos en los métodos getter para devolver una copia en lugar de devolver la referencia al objeto real.

La siguiente clase es un ejemplo que ilustra los conceptos básicos de inmutabilidad. La clase FinalClassExample define los campos y proporciona el método constructor que utiliza una copia profunda para inicializar el objeto. El código en el método main del archivo FinalClassExample.java prueba la inmutabilidad del objeto.

Crear un nuevo archivo llamado FinalClassExample.java y copiar el siguiente código:

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

public final class FinalClassExample {

	// campos de la clase 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;
	}

	// Función Getter para objetos mutables

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

	// Método constructor que realiza una copia profunda
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// La palabra clave "this" se refiere al objeto actual
		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;
	}

	// Prueba la clase inmutable

	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);
		
		// Imprime los valores ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// Cambia los valores de la variable local
		i=20;
		s="modified";
		h1.put("3", "third");
		// Imprime los valores nuevamente
		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 y ejecuta el programa:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Nota: Puede que recibas el siguiente mensaje al compilar el archivo: Nota: FinalClassExample.java utiliza operaciones no verificadas o inseguras porque el método getter está utilizando una conversión no verificada de HashMap<String,String> a Object. Puedes ignorar la advertencia del compilador para los propósitos de este ejemplo.

Obtendrás la siguiente salida:

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}

La salida muestra que los valores del HashMap no cambiaron porque el constructor utiliza una copia profunda y la función getter devuelve un clon del objeto original.

¿Qué sucede cuando no se utiliza la copia profunda y clonación?

Puedes realizar cambios en el archivo FinalClassExample.java para mostrar lo que sucede cuando se utiliza la copia superficial en lugar de la copia profunda y se devuelve el objeto en lugar de una copia. El objeto ya no es inmutable y puede ser modificado. Realiza los siguientes cambios en el archivo de ejemplo (o copia y pega desde el ejemplo de código):

  • Elimina el método del constructor que proporciona la copia profunda y agrega el método del constructor que proporciona la copia superficial resaltado en el siguiente ejemplo.
  • En la función getter, elimina return (HashMap<String, String>) testMap.clone(); y agrega return testMap;.

El archivo de ejemplo debería verse así ahora:

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

public final class FinalClassExample {

	// campos de la clase 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;
	}

	// Función getter para objetos mutables

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

	// Método del constructor realizando la copia superficial

	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;
	}

	// Probar la clase inmutable

	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);
		
		// imprimir los valores de ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// cambiar los valores de la variable local
		i=20;
		s="modified";
		h1.put("3", "third");
		// imprimir los valores nuevamente
		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 y ejecuta el programa:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Obtienes la siguiente salida:

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}

El resultado muestra que los valores del HashMap se cambiaron porque el método del constructor utiliza una copia superficial, hay una referencia directa al objeto original en la función getter.

Conclusión

Has aprendido algunos de los principios generales a seguir cuando creas clases inmutables en Java, incluida la importancia de la copia profunda. Continúa tu aprendizaje con más tutoriales de Java.

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