Питання 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 гарантія, трохи швидше
- 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]]