Як реалізувати шаблон Singleton за допомогою Record
Але найпростіший спосіб — це Enum Singleton, а Record можна використовувати з compact constructor.
🟢 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; }
}
Типові помилки
- Спроба зробити mutable Singleton:
public record Config(String url) { // ❌ Не можна — Record immutable public void setUrl(String url) { } // error } - Множинні екземпляри: ```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]]