Многие наверняка слышали, что 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
  • Возможность написать обобщенную фабрику синглтонов
  • Возможность возвращать отдельный экземпляр для каждого вызывающего его пользователя
undefined
19
Задача
Java Syntax Pro, 19 уровень, 1 лекция
Недоступна
Две реализации одного интерфейса
В интерфейсе Runnable объявлен метод run() и есть две его реализации: в классе Car и в классе Plane. В классе Solution есть публичное статическое поле ArrayList<Runnable> list, а также два публичных статических метода: addToList(Runnable), который добавляет в список list элемент, полученный в качест
undefined
19
Задача
Java Syntax Pro, 19 уровень, 1 лекция
Недоступна
Сортировка по возрасту
В классе Solution есть статическое поле students, которое заполняется студентами (объектами типа Student) в методе main(). У студента (класс Student) есть имя (поле name) и возраст (поле age). Нужно отсортировать в программе студентов по возрасту в убывающем порядке (от старшего к младшему). Для это
undefined
19
Задача
Java Syntax Pro, 19 уровень, 1 лекция
Недоступна
Наставники JavaRush
В классе Solution есть статическое поле mentors, которое заполняется менторами JavaRush (объектами типа JavaRushMentor) в методе main(). У ментора (класс JavaRushMentor) есть имя (поле name). В программе нужно отсортировать менторов по длине имени в возрастающем порядке (от самого короткого до самог
undefined
19
Задача
Java Syntax Pro, 19 уровень, 1 лекция
Недоступна
Знакомство с лямбда-выражением
Перед тобой программа, которая сортирует список чисел по возрастанию. Метод sortNumbers(ArrayList<Integer>) принимает список, элементы которого необходимо отсортировать. Для сортировки используется метод Collections.sort(ArrayList<Integer>, Comparator<Integer>), параметрами которого являются список
undefined
19
Задача
Java Syntax Pro, 19 уровень, 1 лекция
Недоступна
Прощание с лямбда-выражением
Перед тобой программа, которая сортирует список строк по их длине по возрастанию. Это делает метод sortStringsByLength(ArrayList<String>). Для сортировки строк используется метод Collections.sort(ArrayList<String>, Comparator<String>), который принимает список строк и компаратор в виде лямбда-выраже