Багато хто, напевно, чув, що Singleton – це гарне віскі, але оскільки алкоголь шкодить нашому здоров’ю, сьогодні розповімо тобі про синглтон як про патерн проєктування в Java.

Раніше ми вже познайомилися зі створенням об’єктів і знаємо, що для створення Java об’єкта необхідно написати таке:


Robot robot = new Robot(); 
    

Але що якщо мені потрібно, щоб у мене створювався лише один екземпляр класу?

Використовуючи new Robot(), можна створити безліч об’єктів, і ніхто нам це не заборонить. Ось у таких випадках на допомогу саме приходить Singleton.

Наприклад: потрібно написати програму, яка з’єднуватиметься з принтером – тільки ОДНИМ принтером – і даватиме йому завдання друкувати:


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

Начебто звичайний клас, але! Є в ньому одне але: я можу створити кілька екземплярів мого принтера та викликати їх із різних місць. Є ймовірність, що це погано вплине або навіть зламає мій принтер. Тому потрібно зробити так, щоб наш принтер був в одному екземплярі, і допоможе нам Синглтон!

Способи створення Singleton

Є два способи створити Singleton:

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

Розглянемо спочатку спосіб із застосуванням закритого конструктора. Для цього нам потрібно оголосити поле у класі як final та ініціалізувати його. Оскільки воно у нас final, воно у нас буде Immutable, і змінити його ми вже не зможемо.

Також потрібно оголосити конструктор як private, щоб заборонити створення об’єкта поза класом. Це дасть нам гарантію, що екземплярів нашого принтера більше не буде у системі. Конструктор викличеться лише один раз під час ініціалізації та створить нам наш Printer:


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

Ми створили PRINTER синглтон, який буде у нас лише в одному екземплярі, використовуючи закритий конструктор. Змінна PRINTER має модифікатор static, оскільки вона належатиме не об’єкту, а класу Printer.

Тепер розглянемо створення синглтона за допомогою статичного методу для надання доступу до єдиного екземпляра (зверніть увагу, що поле стало private):


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

Скільки разів ми б тут не викликали метод getInstance(), ми завжди отримаємо і той самий екземпляр нашого об’єкта PRINTER.

Створення синглтона за допомогою private конструктора – це, по-перше, більш простий і короткий варіант, а по-друге, API буде очевидним, оскільки поле public оголошено як final, і це гарантує нам, що воно завжди міститиме посилання на один і той самий об’єкт.

Варіант із використанням статичного методу дозволяє нам гнучко змінити синглтон на клас, що не є таким, без зміни його API. Метод getInstance() дає нам єдиний екземпляр нашого об’єкта, але ми можемо його зробити таким чином, щоб повертати окремий екземпляр для кожного користувача, що його викликає.

Також, використовуючи варіант із статичним способом, можна написати узагальнену фабрику синглтонів.

Останньою перевагою статичного методу є можливість використовувати його за допомогою методу rеference

Якщо тобі не потрібна жодна із зазначених вище переваг, тоді рекомендуємо використовувати варіант із public полем.

Якщо нам потрібна серіалізація, то недостатньо буде просто реалізувати інтерфейс Serializable: потрібно ще додати метод readResolve, інакше у разі десеріалізації ми отримуємо новий екземпляр синглтона.

Серіалізація потрібна, щоб зберегти стан об’єкта у послідовності байт, а десеріалізація – відновити об’єкт із байт. Більше про серіалізацію та десеріалізацію можна почитати в цій статті.

Тепер перепишемо наш синглтон:


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

Тут зробимо його сереалізацію і десереалізацію.

Зверни увагу, що приклад нижче — це стандартний механізм серіалізації та десеріалізації в Java. Повне розуміння того, що відбувається в коді, прийде після вивчення тем «Потоки введення-виведення» (модуль Java Syntax) і «Серіалізація» (модуль 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 is: Printer@6be46e8f
Singleton 2 is: 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 is: com.company.Printer@6be46e8f
Singleton 2 is: com.company.Printer@6be46e8f

Метод readResolve() дозволяє отримати той самий об’єкт, який ми і десеріалізували, і тим самим запобігти створенню хибних синглтонів.

Підсумки

Отже, сьогодні ми дізналися про синглтон: як його створювати і коли використовувати, навіщо він потрібен і які варіанти створення є в Java. Нижче – особливості обох варіантів:

Private constructor Static method
  • Простіший і коротший варіант
  • Очевидне API, оскільки поле public final
  • Використання з method reference
  • Можливість написати узагальнену фабрику синглтонів
  • Можливість повертати окремий екземпляр для кожного користувача, що його викликає