Вопрос 27 · Раздел 20

Как реализовать шаблон Singleton с помощью Record

Но самый простой способ — это Enum Singleton, а Record можно использовать с compact constructor.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Record — отличный вариант для Singleton! Начиная с Java 21 (JEP 445), Records поддерживают enhanced serialization, что делает их идеальными для Singleton.

Но самый простой способ — это Enum Singleton, а Record можно использовать с compact constructor.

// Способ 1: Record с static final полем
public record Config(String databaseUrl, int maxConnections) {
    public static final Config INSTANCE = 
        new Config("jdbc:postgresql://localhost:5432/mydb", 10);
}

// Использование
Config config = Config.INSTANCE;

Почему Record хорош для Singleton:

  • ✅ Автоматически immutable
  • ✅ Сериализация безопасна (Java 21+)
  • ✅ Нет проблемы reflection attacks
  • ✅ Минимум кода

🟡 Middle Level

Способы реализации

1. Static field в Record:

public record Logger(String name, LogLevel level) {
    public static final Logger INSTANCE = new Logger("App", LogLevel.INFO);
    
    public void log(String message) {
        System.out.println("[" + name + "] " + message);
    }
}

// Использование
Logger.INSTANCE.log("Hello");

2. Static factory method:

public record DatabaseConnection(String url) {
    private static final DatabaseConnection INSTANCE = 
        new DatabaseConnection("jdbc:postgresql://localhost:5432/db");
    
    public static DatabaseConnection getInstance() {
        return INSTANCE;
    }
    
    public void execute(String sql) {
        // execute query
    }
}

// Использование
DatabaseConnection.getInstance().execute("SELECT 1");

3. Enum Singleton (классический, Joshua Bloch):

public enum Config {
    INSTANCE;
    
    private final String databaseUrl;
    private final int maxConnections;
    
    Config() {
        this.databaseUrl = "jdbc:postgresql://localhost:5432/db";
        this.maxConnections = 10;
    }
    
    public String getDatabaseUrl() { return databaseUrl; }
    public int getMaxConnections() { return maxConnections; }
}

Типичные ошибки

  1. Попытка сделать mutable Singleton:
    public record Config(String url) {
     // ❌ Нельзя — Record immutable
     public void setUrl(String url) { }  // error
    }
    
  2. Множественные экземпляры: ```java public record Config(String url) { // ⚠️ Можно создать новый экземпляр! // new Config(“other-url”) — возможно }

// ✅ Решение — приватный конструктор public record Config(String url) { private static final Config INSTANCE = new Config(“default”);

private Config { }  // private compact constructor

public static Config getInstance() {
    return INSTANCE;
} } ```

🔴 Senior Level

Internal Implementation

Почему Enum лучше Record для Singleton:

// Enum гарантирует единственность на уровне JVM
// Record — нет, можно создать новые экземпляры

public enum EnumSingleton {
    INSTANCE;
    // JVM гарантирует единственный экземпляр
}

public record RecordSingleton(String value) {
    public static final RecordSingleton INSTANCE = new RecordSingleton("default");
    // Можно сделать new RecordSingleton("other") — нет гарантии
}

Serialization safety (Java 21+ JEP 445):

// Record использует enhanced deserialization
// Через canonical constructor вместо readObject

public record Config(String url) implements Serializable {
    private static final Config INSTANCE = new Config("default");
    
    // Deserialization вызывает canonical constructor
    // Можно контролировать через валидацию
    public Config {
        // Валидация при десериализации
    }
}

Архитектурные Trade-offs

Enum vs Record vs Class Singleton:

Подход Guarantee Code Serialization Reflection
Enum ✅ JVM guarantee Минимум ✅ Safe ✅ Protected
Record ❌ No guarantee Минимум ✅ Safe (Java 21+) ⚠️ Can create
Class ❌ Нужно следить Много ⚠️ Нужно readResolve ⚠️ Нужно protect

Edge Cases

1. Thread-safe initialization:

public record Config(String url) {
    // Static инициализация thread-safe по JLS
    private static final Config INSTANCE = new Config("default");
    
    // Lazy initialization
    private static class Holder {
        static final Config INSTANCE = new Config("default");
    }
    
    public static Config getInstance() {
        return Holder.INSTANCE;
    }
}

2. Parameterized Singleton:

public record ServiceRegistry(Map<String, Service> services) {
    private static volatile ServiceRegistry INSTANCE;
    
    public static synchronized ServiceRegistry getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ServiceRegistry(new ConcurrentHashMap<>());
        }
        return INSTANCE;
    }
    
    public void register(String name, Service service) {
        services.put(name, service);
    }
}

3. Reflection protection:

public record Config(String url) {
    private static final Config INSTANCE = new Config("default");

    private Config {
        // Эта проверка НЕ защищает от reflection-атаки.
        // Compact constructor запускается для КАЖДОГО создания, включая INSTANCE.
        // Первая инициализация INSTANCE проходит (INSTANCE ещё null).
        // Но reflection может вызвать конструктор напрямую.
        // Полная защита требует boolean flag или Enum-based подхода.
        if (INSTANCE != null) {
            throw new IllegalStateException("Singleton already created");
        }
    }

    public static Config getInstance() {
        return INSTANCE;
    }
}

Производительность

Singleton creation:
- Static field: 0 ns (class loading time)
- Lazy initialization: ~1 ns после инициализации
- Double-checked locking: ~5 ns

Enum vs Record:
- Enum: JVM guarantee, чуть быстрее
- Record: Одинаково, но можно создать new

Production Experience

Configuration Singleton:

public record AppConfig(
    String databaseUrl,
    int maxConnections,
    Duration timeout,
    Map<String, String> properties
) {
    private static final AppConfig INSTANCE;
    
    static {
        // Load from file/environment
        INSTANCE = loadFromEnvironment();
    }
    
    private static AppConfig loadFromEnvironment() {
        return new AppConfig(
            System.getenv("DB_URL"),
            Integer.parseInt(System.getenv("MAX_CONNECTIONS")),
            Duration.ofSeconds(30),
            Map.of()
        );
    }
    
    public static AppConfig getInstance() {
        return INSTANCE;
    }
}

Best practices:

// ✅ Enum для Singleton — лучшая практика
public enum DatabaseConnection {
    INSTANCE;
    private final Connection conn;

    DatabaseConnection() {
        this.conn = createConnection();
    }

    private Connection createConnection() { /* ... */ }
}

// ✅ Record для immutable configuration
public record Config(String url, int timeout) {
    public static final Config INSTANCE = new Config("default", 30);
}

// ❌ Record без защиты от множественных экземпляров
// ❌ Class Singleton без readResolve
// ❌ Lazy initialization без synchronization

Когда НЕ использовать Singleton

Singleton — спорный паттерн: скрытые зависимости, проблемы с тестируемостью, global state, threading complications. Рассмотрите DI (Spring) как альтернативу.


🎯 Шпаргалка для интервью

Обязательно знать:

  • Enum — лучший Singleton в Java (Joshua Bloch): JVM guarantee единственности
  • Record можно использовать как Singleton через static final INSTANCE поле
  • Record не гарантирует единственность — можно создать new Config("other")
  • Приватный компактный конструктор НЕ защищает от reflection полностью
  • Java 21+ enhanced serialization (JEP 445) делает Record сериализацию безопасной
  • Singleton антипаттерн: скрытые зависимости, проблемы с тестируемостью, DI лучше

Частые уточняющие вопросы:

  • Почему Enum лучше Record для Singleton? — JVM гарантирует единственный экземпляр, reflection protected
  • Можно ли защитить Record от создания новых экземпляров? — Полностью нет, приватный конструктор не защищает от reflection
  • Как Thread-safe Singleton с Record? — Static final инициализация thread-safe по JLS (class loading)
  • Когда НЕ использовать Singleton? — Почти всегда, лучше DI (Spring, CDI)

Красные флаги (НЕ говорить):

  • ❌ “Record гарантирует единственный экземпляр” — Нет, можно создать new
  • ❌ “Приватный конструктор Record защищает от reflection” — Нет, reflection может обойти
  • ❌ “Singleton — лучшая практика” — Спорный паттерн, DI предпочтительнее
  • ❌ “Record Singleton безопасен при сериализации” — Только Java 21+ с JEP 445

Связанные темы:

  • [[1. Что такое Record в Java и с какой версии они доступны]]
  • [[3. Можно ли наследоваться от Record или наследовать Record от другого класса]]
  • [[6. Можно ли переопределить конструктор в Record]]
  • [[8. Можно ли объявлять статические поля и методы в Record]]