Може би сте чували, че едномалцовото шотландско уиски Singleton е добро? Е, алкохолът е вреден за вашето здраве, така че днес instead of това ще ви разкажем за модела на дизайн сингълтон в Java.

По-рано прегледахме създаването на обекти, така че знаем, че за да създадете обект в Java, трябва да напишете нещо като:


Robot robot = new Robot(); 
    

Но Howво ще стане, ако искаме да сме сигурни, че е създаден само един екземпляр от класа?

Новият оператор Robot() може да създава много обекти и нищо не ни спира да го правим. Тук на помощ идва моделът сингълтън.

Да предположим, че трябва да напишете приложение, което ще се свърже с принтер - само ЕДИН принтер - и да му кажете да печата:


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

Това изглежда като обикновен клас... НО! Има едно „но“: мога да създам множество екземпляри на моя принтерен обект и да извиквам методи за тях на различни места. Това може да навреди or дори да повреди моя принтер. Така че трябва да сме сигурни, че има само един екземпляр на нашия принтер и това е, което сингълтън ще направи за нас!

Начини за създаване на сингълтън

Има два начина за създаване на сингълтън:

  • използвайте частен конструктор;
  • експортирайте публичен статичен метод, за да осигурите достъп до един екземпляр.

Нека първо разгледаме използването на частен конструктор. За да направим това, трябва да декларираме поле като final в нашия клас и да го инициализираме. Тъй като го маркирахме като окончателен , знаем, че ще бъде неизменен , т.е. вече не можем да го променяме.

Трябва също така да декларирате конструктора като частен , за да предотвратите създаването на обекти извън класа . Това ни гарантира, че няма да има други копия на нашия принтер в програмата. Конструкторът ще бъде извикан само веднъж по време на инициализацията и ще създаде нашия принтер :


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

Използвахме частен конструктор, за да създадем сингълтон на PRINTER — винаги ще има само един екземпляр. TheПРИНТЕРпроменливата има статичен модификатор , тъй като принадлежи не на който и да е обект, а на самия клас Printer .

Сега нека помислим за създаването на единичен елемент, използвайки статичен метод, за да предоставим достъп до един екземпляр от нашия клас (и имайте предвид, че полето вече е частно ):


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

Без meaning колко пъти извикваме метода getInstance() тук, винаги ще получаваме същотоПРИНТЕРобект.

Създаването на сингълтън с помощта на частен конструктор е по-просто и по-сбито. Нещо повече, API е очевиден, тъй като публичното поле е декларирано като final , което гарантира, че винаги ще съдържа препратка към един и същ обект.

Опцията за статичен метод ни дава гъвкавостта да променим единичния клас в не-единичен клас, без да променяме неговия API. Методът getInstance() ни дава единичен екземпляр на нашия обект, но можем да го променим така, че да връща отделен екземпляр за всеки потребител, който го извиква.

Статичната опция също така ни позволява да напишем обща фабрика за единичен елемент.

Крайното предимство на статичната опция е, че можете да я използвате с препратка към метод.

Ако не се нуждаете от някое от горните предимства, тогава препоръчваме да използвате опцията, която включва публично поле.

Ако имаме нужда от сериализация, тогава няма да е достатъчно просто да имплементираме Serializable интерфейса. Също така трябва да добавим метода readResolve , в противен случай ще получим нов сингълтон екземпляр по време на десериализацията.

Сериализацията е необходима, за да се запази състоянието на обект като последователност от byteове, а десериализацията е необходима, за да се възстанови обектът от тези byteове. Можете да прочетете повече за сериализацията и десериализацията в тази статия .

Сега нека пренапишем нашия сингълтон:


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

Сега ще го сериализираме и десериализираме.

Имайте предвид, че примерът по-долу е стандартният механизъм за сериализация и десериализация в Java. Пълното разбиране на случващото се в codeа ще дойде, след като изучите „I/O streams“ (в модула Java Syntax) и „Serialization“ (в модула 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);
    

И получаваме този резултат:

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

Тук виждаме, че десериализацията ни даде различен екземпляр на нашия сингълтън. За да поправим това, нека добавим метода readResolve към нашия клас:


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

Сега ще сериализираме и десериализираме нашия сингълтон отново:


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

И получаваме:

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

Методът readResolve() ни позволява да получим същия обект, който сме десериализирали, като по този начин предотвратява създаването на фалшиви сингълтони.

Резюме

Днес научихме за singletons: How да ги създаваме и кога да ги използваме, за Howво служат и Howви опции предлага Java за създаването им. Специфичните характеристики на двата варианта са дадени по-долу:

Частен строител Статичен метод
  • По-лесно и по-сбито
  • Очевиден API, тъй като полето за сингълтон е публичен финал
  • Може да се използва с препратка към метод
  • Може да се използва за писане на генерична сингълтън фабрика
  • Може да се използва за връщане на отделен екземпляр за всеки потребител