Singleton

Модуль 1. Java Syntax
20 уровень , 1 лекция
Открыта

Многие наверняка слышали, что 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
  • Возможность написать обобщенную фабрику синглтонов
  • Возможность возвращать отдельный экземпляр для каждого вызывающего его пользователя
Комментарии (36)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Andrei A. Уровень 67
27 марта 2025
Только после лекции Андрея Мазикова стало понятнее. Респект Андрею!
Михаил Уровень 89
10 октября 2024
Виски, как и кофе, мужского рода. Поэтому не "хорошее виски", а "хороший виски". Так, только так и никак иначе.
Олег Уровень 111 Expert
5 сентября 2024
Что такое Singleton Singleton — паттерн проектирования, который гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру. Этот паттерн полезен, когда необходимо, чтобы объект существовал в единственном экземпляре и мог управлять общими ресурсами, такими как кэш, доступ к базе данных, настройки или логирование. Когда использовать Singleton Использование Singleton целесообразно в следующих ситуациях: - Когда нужно гарантировать, что в системе существует только один экземпляр объекта (например, логгер, менеджер базы данных, кэш). - Когда требуется единая точка доступа для управления ресурсами. - Когда важно сохранить состояние в одной глобальной сущности. Принципы создания Singleton - Приватный конструктор: предотвращает создание экземпляров класса извне. - Статическое поле для хранения экземпляра: для хранения единственного экземпляра Singleton. - Статический метод для получения экземпляра: предоставляет глобальную точку доступа к единственному экземпляру класса.
Олег Уровень 111 Expert
5 сентября 2024
Варианты реализации Singleton в Java 1. Ленивая инициализация (Lazy Initialization) Объект создается при первом обращении к методу `getInstance()`. Однако это решение не потокобезопасно.

public class SingletonLazy {
    private static SingletonLazy instance;
    private SingletonLazy() {}
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}
2. Ленивая инициализация с синхронизацией (Thread-Safe Lazy Initialization) Использование ключевого слова `synchronized` для потокобезопасного доступа к экземпляру. Однако синхронизация может снижать производительность при множественных вызовах.

public class SingletonLazyThreadSafe {
    private static SingletonLazyThreadSafe instance;
    private SingletonLazyThreadSafe() {}
    public static synchronized SingletonLazyThreadSafe getInstance() {
        if (instance == null) {
            instance = new SingletonLazyThreadSafe();
        }
        return instance;
    }
}
Олег Уровень 111 Expert
5 сентября 2024
3. Двойная проверка блокировки (Double-Checked Locking) Этот способ улучшает производительность в многопоточной среде за счет двойной проверки: перед и после синхронизации. Использование ключевого слова `volatile` гарантирует корректную работу с многопоточностью.

public class SingletonDoubleCheckedLocking {
    private static volatile SingletonDoubleCheckedLocking instance;
    private SingletonDoubleCheckedLocking() {}
    public static SingletonDoubleCheckedLocking getInstance() {
        if (instance == null) {
            synchronized (SingletonDoubleCheckedLocking.class) {
                if (instance == null) {
                    instance = new SingletonDoubleCheckedLocking();
                }
            }
        }
        return instance;
    }
}
4. Статическая инициализация (Eager Initialization) В этом случае экземпляр синглтона создается при загрузке класса. Подходит для случаев, когда создание объекта не накладно и гарантированно потребуется.

public class SingletonEager {
    private static final SingletonEager instance = new SingletonEager();
    private SingletonEager() {}
    public static SingletonEager getInstance() {
        return instance;
    }
}
5. Статический блок инициализации (Static Block Initialization) Этот вариант позволяет обрабатывать возможные исключения при создании экземпляра.

public class SingletonStaticBlock {
    private static SingletonStaticBlock instance;
    static {
        try {
            instance = new SingletonStaticBlock();
        } catch (Exception e) {
            throw new RuntimeException("Ошибка при создании Singleton");
        }
    }
    private SingletonStaticBlock() {}
    public static SingletonStaticBlock getInstance() {
        return instance;
    }
}
Олег Уровень 111 Expert
5 сентября 2024
6. Реализация через вложенный класс (Bill Pugh Singleton) Этот способ считается одним из лучших, поскольку он не требует синхронизации, обеспечивая при этом ленивую инициализацию.

public class SingletonBillPugh {
    private SingletonBillPugh() {}
    private static class SingletonHelper {
        private static final SingletonBillPugh INSTANCE = new SingletonBillPugh();
    }
    public static SingletonBillPugh getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
7. Реализация с помощью `enum` (Enum Singleton) Это самый простой и безопасный способ создания синглтона. Он гарантированно защищен от сериализации и рефлексии.

public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {
        System.out.println("Doing something...");
    }
}
Преимущества Singleton: 1. Единственность: гарантируется существование только одного экземпляра. 2. Глобальная точка доступа: к экземпляру можно обратиться из любой части программы. 3. Ленивая инициализация: объект создается только при необходимости (в ленивых реализациях). 4. Упрощенное управление ресурсами: единый объект для кэша, логирования, конфигурации и т.д. Недостатки Singleton: 1. Затруднения при тестировании: из-за глобальной природы сложно замещать зависимости при тестировании. 2. Многопоточность: неправильно реализованные синглтоны могут вызывать проблемы в многопоточных средах. 3. Нарушение принципов ООП: Singleton может нарушить принципы SOLID, например, принцип единственной ответственности (SRP).
Farhad Уровень 1
26 июля 2024
Просто и понятно =)
Булат Уровень 109
24 июля 2024
идеально было бы , обширней бы описать что выполняет каждый код, лекция прям реально непонятная. ничего не понял
Andrej (Drew) Уровень 112 Expert
11 июля 2024
Прекрасная лекция (нет) 😑
Дмитрий Уровень 49
5 июня 2024
Вижу лекцию Андрея Мазикова, ставлю лайк неглядя!