Peut-être avez-vous entendu dire que le whisky écossais single malt Singleton est bon ? Eh bien, l'alcool est mauvais pour la santé, alors aujourd'hui, nous allons plutôt vous parler du modèle de conception singleton en Java.

Nous avons déjà passé en revue la création d'objets, nous savons donc que pour créer un objet en Java, vous devez écrire quelque chose comme :

Robot robot = new Robot();

Mais que se passe-t-il si nous voulons nous assurer qu'une seule instance de la classe est créée ?

La nouvelle instruction Robot() peut créer de nombreux objets, et rien ne nous empêche de le faire. C'est là que le modèle singleton vient à la rescousse.

Supposons que vous ayez besoin d'écrire une application qui se connectera à une imprimante - une seule imprimante - et lui dira d'imprimer :

public class Printer {

	public Printer() {
	}

	public void print() {}
}

Cela ressemble à une classe ordinaire... MAIS ! Il y a un "mais": je peux créer plusieurs instances de mon objet imprimante et appeler des méthodes dessus à différents endroits. Cela pourrait endommager ou même casser mon imprimante. Nous devons donc nous assurer qu'il n'y a qu'une seule instance de notre imprimante, et c'est ce qu'un singleton fera pour nous !

Façons de créer un singleton

Il existe deux manières de créer un singleton :

  • utiliser un constructeur privé ;
  • exporter une méthode statique publique pour fournir l'accès à une seule instance.

Considérons d'abord l'utilisation d'un constructeur privé. Pour ce faire, nous devons déclarer un champ comme final dans notre classe et l'initialiser. Puisque nous l'avons marqué comme final , nous savons qu'il sera immuable , c'est-à-dire que nous ne pourrons plus le modifier.

Vous devez également déclarer le constructeur comme privé pour empêcher la création d'objets en dehors de la classe . Cela nous garantit qu'il n'y aura pas d'autres instances de notre imprimante dans le programme. Le constructeur sera appelé une seule fois lors de l'initialisation et créera notre Printer :

public class Printer {

	public static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public void print() {
        // Printing...

	}
}

Nous avons utilisé un constructeur privé pour créer un singleton PRINTER — il n'y aura jamais qu'une seule instance. LeIMPRIMANTELa variable a le modificateur static , car elle n'appartient à aucun objet, mais à la classe Printer elle-même.

Considérons maintenant la création d'un singleton à l'aide d'une méthode statique pour donner accès à une seule instance de notre classe (et notez que le champ est maintenant private ):

public class Printer {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public static Printer getInstance() {
    	return PRINTER;
	}

	public void print() {
        // Printing...
	}
}

Peu importe combien de fois nous appelons la méthode getInstance() ici, nous obtiendrons toujours le mêmeIMPRIMANTEobjet.

La création d'un singleton à l'aide d'un constructeur privé est plus simple et plus concise. De plus, l'API est évidente, puisque le champ public est déclaré comme final , ce qui garantit qu'il contiendra toujours une référence au même objet.

L'option de méthode statique nous donne la flexibilité de changer le singleton en une classe non singleton sans changer son API. La méthode getInstance() nous donne une seule instance de notre objet, mais nous pouvons la modifier afin qu'elle renvoie une instance distincte pour chaque utilisateur qui l'appelle.

L'option statique nous permet également d'écrire une fabrique de singleton générique.

Le dernier avantage de l'option statique est que vous pouvez l'utiliser avec une référence de méthode.

Si vous n'avez besoin d'aucun des avantages ci-dessus, nous vous recommandons d'utiliser l'option qui implique un champ public .

Si nous avons besoin de sérialisation, il ne suffira pas d'implémenter simplement l' interface Serializable . Nous devons également ajouter la méthode readResolve , sinon nous obtiendrons une nouvelle instance singleton lors de la désérialisation.

La sérialisation est nécessaire pour enregistrer l'état d'un objet sous la forme d'une séquence d'octets, et la désérialisation est nécessaire pour restaurer l'objet à partir de ces octets. Vous pouvez en savoir plus sur la sérialisation et la désérialisation dans cet article .

Réécrivons maintenant notre singleton :

public class Printer implements Serializable {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public static Printer getInstance() {
    	return PRINTER;
	}
}

Nous allons maintenant le sérialiser et le désérialiser.

Notez que l'exemple ci-dessous est le mécanisme standard de sérialisation et de désérialisation en Java. Une compréhension complète de ce qui se passe dans le code viendra après avoir étudié les "flux d'E/S" (dans le module Java Syntax) et la "sérialisation" (dans le module Java Core).
var printer = Printer.getInstance();
var fileOutputStream = new FileOutputStream("printer.txt");
var objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(printer);
objectOutputStream.close();

var fileInputStream = new FileInputStream("printer.txt");
var objectInputStream = new ObjectInputStream(fileInputStream);
var deserializedPrinter =(Printer) objectInputStream.readObject();
objectInputStream.close();

System.out.println("Singleton 1 is: " + printer);
System.out.println("Singleton 2 is: " + deserializedPrinter);

Et nous obtenons ce résultat :

Le singleton 1 est : Printer@6be46e8f
Le singleton 2 est : Printer@3c756e4d

Ici, nous voyons que la désérialisation nous a donné une instance différente de notre singleton. Pour résoudre ce problème, ajoutons la méthode readResolve à notre classe :

public class Printer implements Serializable {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public static Printer getInstance() {
    	return PRINTER;
	}

	public Object readResolve() {
    	return PRINTER;
	}
}

Nous allons maintenant sérialiser et désérialiser à nouveau notre singleton :

var printer = Printer.getInstance();
var fileOutputStream = new FileOutputStream("printer.txt");
var objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(printer);
objectOutputStream.close();

var fileInputStream = new FileInputStream("printer.txt");
var objectInputStream = new ObjectInputStream(fileInputStream);
var deserializedPrinter=(Printer) objectInputStream.readObject();
objectInputStream.close();

System.out.println("Singleton 1 is: " + printer);
System.out.println("Singleton 2 is: " + deserializedPrinter);

Et on obtient :

Le singleton 1 est : com.company.Printer@6be46e8f
Le singleton 2 est : com.company.Printer@6be46e8f

La méthode readResolve() nous permet d'obtenir le même objet que nous avons désérialisé, empêchant ainsi la création de singletons escrocs.

Résumé

Aujourd'hui, nous avons découvert les singletons : comment les créer et quand les utiliser, à quoi ils servent et quelles options Java offre pour les créer. Les spécificités des deux options sont données ci-dessous :

Constructeur privé Méthode statique
  • Plus simple et plus concis
  • API évidente puisque le champ singleton est public final
  • Peut être utilisé avec une référence de méthode
  • Peut être utilisé pour écrire une fabrique de singletons génériques
  • Peut être utilisé pour renvoyer une instance distincte pour chaque utilisateur