Recherche de site Web

Comment créer une classe immuable en Java


Introduction

Cet article fournit un aperçu de la manière de créer une classe immuable en programmation Java.

Un objet est immuable lorsque son état ne change pas après avoir été initialisé. Par exemple, String est une classe immuable et, une fois instancié, la valeur d'un objet String ne change jamais. Apprenez-en davantage sur les raisons pour lesquelles la classe String est immuable en Java.

Parce qu'un objet immuable ne peut pas être mis à jour, les programmes doivent créer un nouvel objet à chaque changement d'état. Cependant, les objets immuables présentent également les avantages suivants :

  • Une classe immuable est bonne pour les purposes de mise en cache car vous n'avez pas à vous soucier des changements de valeur.
  • Une classe immuable est intrinsèquement sûre pour les threads, donc vous n'avez pas à vous soucier de la sécurité des threads dans des environnements multi-threads.

En savoir plus sur le multi-threading en Java et consulter les questions d'entretien sur le multi-threading en Java.

Créer une classe immuable en Java

Pour créer une classe immuable en Java, vous devez suivre ces principes généraux :

  1. Déclarez la classe comme final afin qu’elle ne puisse pas être étendue.
  2. Rendez tous les champs private afin que l'accès direct ne soit pas autorisé.
  3. Ne fournissez pas de méthodes de définition pour les variables.
  4. Rendez tous les champs modifiables final afin que la valeur d’un champ ne puisse être attribuée qu’une seule fois.
  5. Initialisez tous les champs en utilisant une méthode constructeur effectuant une copie profonde.
  6. Effectuez le clonage des objets dans les méthodes getter pour retourner une copie plutôt que de retourner la référence réelle de l'objet.

La classe suivante est un exemple qui illustre les bases de l'immuabilité. La classe FinalClassExample définit les champs et fournit la méthode constructeur qui utilise une copie profonde pour initialiser l'objet. Le code dans la méthode main du fichier FinalClassExample.java teste l'immuabilité de l'objet.

Créez un nouveau fichier appelé FinalClassExample.java et copiez-y le code suivant :

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

public final class FinalClassExample {

	// fields of the FinalClassExample class
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Getter function for mutable objects

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

	// Constructor method performing deep copy
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// "this" keyword refers to the current object
		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 the immutable class

	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);
		
		// print the ce values
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// change the local variable values
		i=20;
		s="modified";
		h1.put("3", "third");
		// print the values again
		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());

	}

}

Compiler et exécuter le programme :

javac FinalClassExample.java

java FinalClassExample

Remarque : Vous pourriez recevoir le message suivant lors de la compilation du fichier : Note : FinalClassExample.java utilise des opérations non vérifiées ou non sûres parce que la méthode getter utilise un cast non vérifié de HashMap<String,String> vers Object. Vous pouvez ignorer l'avertissement du compilateur pour les besoins de cet exemple.

Vous obtenez la sortie suivante :

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 sortie montre que les valeurs du HashMap n'ont pas changé parce que le constructeur utilise une copie profonde et que la fonction getter renvoie un clone de l'objet original.

Que se passe-t-il lorsque vous n'utilisez pas la copie profonde et le clonage

Vous pouvez apporter des modifications au fichier FinalClassExample.java pour montrer ce qui se passe lorsque vous utilisez une copie superficielle au lieu d'une copie profonde et retournez l'objet au lieu d'une copie. L'objet n'est plus immuable et peut être modifié. Apportez les modifications suivantes au fichier exemple (ou copiez et collez à partir de l'exemple de code) :

  • Supprimez la méthode constructeur fournissant une copie profonde et ajoutez la méthode constructeur fournissant une copie superficielle qui est mise en évidence dans l'exemple suivant.
  • Dans la fonction getter, supprimez return (HashMap<String, String>) testMap.clone(); et ajoutez return testMap;.

Le fichier exemple devrait maintenant ressembler à ceci :

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

public final class FinalClassExample {

	// fields of the FinalClassExample class
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Getter function for mutable objects

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

	//Constructor method performing shallow copy

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

	// Test the immutable class

	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);
		
		// print the ce values
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// change the local variable values
		i=20;
		s="modified";
		h1.put("3", "third");
		// print the values again
		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());

	}

}

Compiler et exécuter le programme :

javac FinalClassExample.java

java FinalClassExample

Vous obtenez la sortie suivante :

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}

La sortie montre que les valeurs de HashMap ont été modifiées parce que la méthode du constructeur utilise une copie superficielle et qu'il y a une référence directe à l'objet original dans la fonction getter.

Conclusion

Vous avez appris certains des principes généraux à suivre lorsque vous créez des classes immuables en Java, y compris l'importance de la copie profonde. Continuez votre apprentissage avec plus de tutoriels Java.