Vielleicht haben Sie gehört, dass Singleton Single Malt Scotch Whisky gut ist? Nun, Alkohol ist gesundheitsschädlich, deshalb werden wir Ihnen heute stattdessen das Singleton-Entwurfsmuster in Java vorstellen.

Wir haben uns zuvor mit der Erstellung von Objekten befasst und wissen daher, dass Sie zum Erstellen eines Objekts in Java Folgendes schreiben müssen:


Robot robot = new Robot(); 
    

Was aber, wenn wir sicherstellen möchten, dass nur eine Instanz der Klasse erstellt wird?

Die neue Robot()- Anweisung kann viele Objekte erstellen, und nichts hindert uns daran. Hier kommt das Singleton-Muster zur Rettung.

Angenommen, Sie müssen eine Anwendung schreiben, die eine Verbindung zu einem Drucker – nur EINEM Drucker – herstellt und ihn zum Drucken anweist:


public class Printer { 
 
	public Printer() { 
	} 
     
	public void print() { 
    	… 
	} 
}
    

Das sieht aus wie eine gewöhnliche Klasse... ABER! Es gibt ein „Aber“: Ich kann mehrere Instanzen meines Druckerobjekts erstellen und an verschiedenen Stellen Methoden dafür aufrufen. Dies könnte meinen Drucker beschädigen oder sogar zerstören. Wir müssen also sicherstellen, dass es nur eine Instanz unseres Druckers gibt, und genau das erledigt ein Singleton für uns!

Möglichkeiten zum Erstellen eines Singletons

Es gibt zwei Möglichkeiten, einen Singleton zu erstellen:

  • Verwenden Sie einen privaten Konstruktor.
  • Exportieren Sie eine öffentliche statische Methode, um Zugriff auf eine einzelne Instanz bereitzustellen.

Betrachten wir zunächst die Verwendung eines privaten Konstruktors. Dazu müssen wir in unserer Klasse ein Feld als final deklarieren und es initialisieren. Da wir es als final markiert haben , wissen wir, dass es unveränderlich sein wird , dh wir können es nicht mehr ändern.

Sie müssen den Konstruktor außerdem als privat deklarieren , um zu verhindern, dass Objekte außerhalb der Klasse erstellt werden . Dies garantiert uns, dass es keine weiteren Instanzen unseres Druckers im Programm gibt. Der Konstruktor wird während der Initialisierung nur einmal aufgerufen und erstellt unseren Drucker :


public class Printer { 
     
	public static final Printer PRINTER = new Printer(); 
     
	private Printer() { 
	} 
 
	public void print() { 
        // Printing... 
 
	} 
}
    

Wir haben einen privaten Konstruktor verwendet, um einen PRINTER-Singleton zu erstellen – es wird immer nur eine Instanz geben. DerDRUCKERDie Variable hat den statischen Modifikator , da sie nicht zu einem Objekt, sondern zur Druckerklasse selbst gehört.

Betrachten wir nun die Erstellung eines Singletons mithilfe einer statischen Methode, um Zugriff auf eine einzelne Instanz unserer Klasse zu ermöglichen (und beachten Sie, dass das Feld jetzt privat ist ):


public class Printer { 
 
	private static final Printer PRINTER = new Printer(); 
 
	private Printer() { 
	} 
 
	public static Printer getInstance() { 
    	return PRINTER; 
	} 
     
	public void print() { 
        // Printing... 
	} 
} 
    

Egal wie oft wir hier die Methode getInstance() aufrufen , wir erhalten immer das GleicheDRUCKERObjekt.

Das Erstellen eines Singletons mit einem privaten Konstruktor ist einfacher und prägnanter. Darüber hinaus ist die API offensichtlich, da das öffentliche Feld als final deklariert ist , was garantiert, dass es immer einen Verweis auf dasselbe Objekt enthält.

Die statische Methodenoption gibt uns die Flexibilität, den Singleton in eine Nicht-Singleton-Klasse zu ändern, ohne seine API zu ändern. Die getInstance()- Methode gibt uns eine einzelne Instanz unseres Objekts, aber wir können sie so ändern, dass sie für jeden Benutzer, der sie aufruft, eine separate Instanz zurückgibt.

Mit der statischen Option können wir auch eine generische Singleton-Factory schreiben.

Der letzte Vorteil der statischen Option besteht darin, dass Sie sie mit einer Methodenreferenz verwenden können.

Wenn Sie keinen der oben genannten Vorteile benötigen, empfehlen wir Ihnen die Nutzung eines öffentlichen Feldes.

Wenn wir Serialisierung benötigen, reicht es nicht aus, nur die Serializable- Schnittstelle zu implementieren. Wir müssen auch die readResolve- Methode hinzufügen, sonst erhalten wir während der Deserialisierung eine neue Singleton-Instanz.

Serialisierung ist erforderlich, um den Status eines Objekts als Folge von Bytes zu speichern, und Deserialisierung ist erforderlich, um das Objekt aus diesen Bytes wiederherzustellen. Weitere Informationen zur Serialisierung und Deserialisierung finden Sie in diesem Artikel .

Schreiben wir nun unseren Singleton um:


public class Printer implements Serializable { 
 
	private static final Printer PRINTER = new Printer(); 
 
	private Printer() { 
	} 
 
	public static Printer getInstance() { 
    	return PRINTER; 
	} 
} 
    

Jetzt werden wir es serialisieren und deserialisieren.

Beachten Sie, dass das folgende Beispiel der Standardmechanismus für die Serialisierung und Deserialisierung in Java ist. Ein vollständiges Verständnis dessen, was im Code geschieht, erhalten Sie, nachdem Sie „I/O-Streams“ (im Java-Syntax-Modul) und „Serialisierung“ (im Java-Core-Modul) studiert haben.

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

Und wir erhalten dieses Ergebnis:

Singleton 1 ist: Printer@6be46e8f
Singleton 2 ist: Printer@3c756e4d

Hier sehen wir, dass wir durch Deserialisierung eine andere Instanz unseres Singletons erhalten haben. Um dies zu beheben, fügen wir unserer Klasse die Methode readResolve hinzu:


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

Jetzt serialisieren und deserialisieren wir unseren Singleton erneut:


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

Und wir bekommen:

Singleton 1 ist: com.company.Printer@6be46e8f
Singleton 2 ist: com.company.Printer@6be46e8f

Mit der readResolve()- Methode können wir dasselbe Objekt abrufen, das wir deserialisiert haben, und so die Erstellung unerwünschter Singletons verhindern.

Zusammenfassung

Heute haben wir etwas über Singletons gelernt: wie man sie erstellt und wann man sie verwendet, wozu sie dienen und welche Möglichkeiten Java für ihre Erstellung bietet. Die spezifischen Merkmale beider Optionen sind nachstehend aufgeführt:

Privater Konstrukteur Statische Methode
  • Einfacher und prägnanter
  • Offensichtliche API, da das Singleton-Feld öffentlich und endgültig ist
  • Kann mit einer Methodenreferenz verwendet werden
  • Kann zum Schreiben einer generischen Singleton-Factory verwendet werden
  • Kann verwendet werden, um für jeden Benutzer eine separate Instanz zurückzugeben