Wielu pewnie słyszało, że Singleton to dobra whisky, ale ponieważ alkohol jest szkodliwy dla naszego zdrowia, dzisiaj opowiemy Wam o singletonie jako wzorcu projektowym w Javie.

Wcześniej spotkaliśmy się już z tworzeniem obiektów i wiemy, że aby stworzyć obiekt Java, należy napisać:


Robot robot = new Robot(); 
    

Ale co, jeśli chcę, aby utworzono dla mnie tylko jedną instancję klasy?

Za pomocą new Robot() możemy stworzyć wiele obiektów i nikt nam tego nie zabroni. W tym momencie na ratunek przychodzi Singleton.

Na przykład: musisz napisać aplikację, która połączy się z drukarką - tylko JEDNĄ drukarką - i zleci jej drukowanie:


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

Wydaje się, że to normalna klasa, ale! Ma jedno ale: mogę utworzyć wiele instancji mojej drukarki i wywoływać je z różnych miejsc. Istnieje możliwość, że wpłynie to źle na moją drukarkę lub nawet ją uszkodzi. Dlatego musimy zadbać o to, aby nasza drukarka była w jednym egzemplarzu, a właśnie Singleton nam w tym pomoże!

Sposoby tworzenia singletona

Istnieją dwa sposoby na utworzenie Singletona:

  • użyj prywatnego konstruktora;
  • wyeksportuj publiczną metodę statyczną, aby zapewnić dostęp do pojedynczej instancji.

Rozważ najpierw metodę używającą prywatnego konstruktora. W tym celu musimy zadeklarować pole w klasie jako final i zainicjować je. Ponieważ mamy to ostateczne , będziemy mieć to Niezmienne i nie będziemy mogli tego zmienić.

Należy również zadeklarować konstruktora jako prywatnego , aby uniemożliwić utworzenie obiektu poza klasą . Da nam to gwarancję, że w systemie nie będzie więcej instancji naszej drukarki. Konstruktor zostanie wywołany tylko raz podczas inicjalizacji i stworzy dla nas naszą drukarkę :


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

Stworzyliśmy singleton PRINTER, którego będziemy mieć tylko jedną instancję, używając prywatnego konstruktora. ZmiennyDRUKARKAma modyfikator static , ponieważ nie będzie należeć do obiektu, ale do klasy Printer .

Teraz rozważ utworzenie singletonu przy użyciu metody statycznej, aby zapewnić dostęp do pojedynczej instancji (zwróć uwagę, że pole stało się private ):


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

Bez względu na to, ile razy wywołamy tutaj metodę getInstance() , zawsze otrzymamy tę samą instancję naszego obiektuDRUKARKA.

Stworzenie singletona z prywatnym konstruktorem jest po pierwsze prostszą i krótszą opcją, a po drugie API będzie oczywiste, ponieważ pole publiczne jest zadeklarowane jako final , a to gwarantuje nam, że zawsze będzie zawierało odwołanie do jednego i drugiego ten sam przedmiot.

Opcja metody statycznej pozwala nam elastycznie zmieniać singleton na nieklasowy bez zmiany jego API. Metoda getInstance() daje nam pojedynczą instancję naszego obiektu, ale możemy to zrobić tak, aby zwracała osobną instancję dla każdego użytkownika, który ją wywołuje.

Ponadto, używając opcji static way, możesz napisać ogólną fabrykę singletonów.

Ostatnią zaletą metody statycznej jest możliwość użycia jej z metodą referencyjną.

Jeśli nie potrzebujesz żadnej z powyższych zalet, zalecamy skorzystanie z opcji z polem publicznym .

Jeśli potrzebujemy serializacji, to nie wystarczy zaimplementować interfejsu Serializable : musimy jeszcze dodać metodę readResolve , w przeciwnym razie podczas deserializacji otrzymamy nową instancję singletona.

Serializacja jest potrzebna do przechowywania stanu obiektu w sekwencji bajtów, a deserializacja jest potrzebna do przywrócenia obiektu z bajtów. Więcej informacji na temat serializacji i deserializacji można znaleźć w tym artykule .

Teraz przepiszmy nasz singleton:


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

Tutaj dokonamy serializacji i deserializacji.

Zauważ, że poniższy przykład jest standardowym mechanizmem serializacji i deserializacji Java. Pełne zrozumienie tego, co dzieje się w kodzie, nastąpi po przestudiowaniu tematów „Strumienie we/wy” (moduł Java Syntax) i „Serializacja” (moduł 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);
    

I otrzymujemy wynik:

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

Tutaj widzimy, że podczas deserializacji otrzymaliśmy kolejną klasę naszego singletona. Aby to naprawić, dodajmy metodę readResolve do naszej klasy :


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

Teraz ponownie zrealizuj i zderealizuj nasz 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); 
    

I otrzymujemy:

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

Metoda readResolve() pozwala uzyskać ten sam obiekt, który deserializowaliśmy, zapobiegając w ten sposób tworzeniu fałszywych singletonów.

Wyniki

Więc dzisiaj dowiedzieliśmy się o singletonie: jak go stworzyć i kiedy go używać, do czego służy i jakie opcje jego tworzenia są w Javie. Poniżej znajdują się cechy obu opcji:

Prywatny konstruktor metoda statyczna
  • Łatwiejsza i krótsza wersja
  • Oczywisty interfejs API, ponieważ pole jest ostateczną wersją publiczną
  • Użyj z odwołaniem do metody
  • Możliwość napisania generycznej fabryki singletonów
  • Możliwość zwrócenia osobnej instancji dla każdego wywołującego ją użytkownika