Що таке патерн Prototype?
4. Не працює з final полями 5. Checked exception CloneNotSupportedException
🟢 Junior Level
Prototype — патерн, який дозволяє створювати нові об’єкти шляхом копіювання наявного. Copy constructor — рекомендована Java-альтернатива Object.clone().
Проста аналогія: Ксерокс документів. Замість того щоб щоразу писати документ з нуля, ви копіюєте готовий шаблон і змінюєте потрібні поля.
Приклад:
// Конструктор копіювання
public class User {
private String name;
private List<String> permissions;
// Звичайний конструктор
public User(String name, List<String> permissions) {
this.name = name;
this.permissions = new ArrayList<>(permissions);
}
// Конструктор копіювання (Prototype)
public User(User other) {
this.name = other.name;
this.permissions = new ArrayList<>(other.permissions); // Копіюємо список!
}
}
// Використання
User template = new User("admin", List.of("read", "write"));
User copy = new User(template); // Копія шаблону!
Коли використовувати:
- Створення об’єкта дороге (запит до БД, мережі)
- Потрібно багато схожих об’єктів
- Конструктор складний
🟡 Middle Level
Чому Cloneable у Java — поганий
// ❌ Проблеми з Cloneable
public class User implements Cloneable {
private String name;
private List<String> permissions;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // Поверхнева копія!
}
}
User original = new User("Ivan", new ArrayList<>(List.of("read")));
User copy = (User) original.clone();
// ❌ Поверхнева копія:
copy.getPermissions().add("admin");
// original.getPermissions() теж змінився!
Проблеми Cloneable:
- Не містить методу
clone()(маркерний інтерфейс) clone()захищений — потрібно перевизначати- Поверхнева копія за замовчуванням
- Не працює з
finalполями - Checked exception
CloneNotSupportedException
Рішення: Конструктор копіювання
// ✅ Рекомендований підхід (Joshua Bloch)
public class User {
private final String name;
private final List<String> permissions;
// Конструктор копіювання
public User(User other) {
this.name = other.name;
this.permissions = new ArrayList<>(other.permissions); // Глибока копія!
}
// Статичний фабричний метод
public static User copyOf(User other) {
return new User(other);
}
}
User original = new User("Ivan", List.of("read"));
User copy = User.copyOf(original); // Зрозуміло і типобезпечно
Глибока vs Поверхнева копія
// Поверхнева (Shallow) — копіює посилання
public User(User other) {
this.name = other.name; // String — іммутабельна, ок
this.permissions = other.permissions; // Посилання на той самий список! ❌
}
// Глибока (Deep) — копіює все
public User(User other) {
this.name = other.name;
this.permissions = new ArrayList<>(other.permissions); // Новий список ✅
this.address = new Address(other.address); // Копіюємо вкладений об'єкт
}
Prototype Scope у Spring
// Prototype = новий екземпляр при кожному запиті
@Component
@Scope("prototype")
public class OrderProcessor {
private final Order order;
public OrderProcessor(Order order) {
this.order = order; // Кожного разу новий Order
}
}
// Використання
OrderProcessor p1 = context.getBean(OrderProcessor.class);
OrderProcessor p2 = context.getBean(OrderProcessor.class);
// p1 != p2 → різні об'єкти!
// ⚠️ Пастка: Prototype у Singleton
@Component // Singleton!
public class OrderService {
@Autowired
private OrderProcessor processor; // Створиться ОДИН раз!
}
// Рішення 1: @Lookup
@Component
public abstract class OrderService {
@Lookup
protected abstract OrderProcessor getProcessor();
}
// Рішення 2: ObjectProvider
@Component
public class OrderService {
private final ObjectProvider<OrderProcessor> processorProvider;
public void process() {
OrderProcessor processor = processorProvider.getObject(); // Новий кожен раз!
}
}
Клонування через серіалізацію
// Для складних об'єктів з глибокою вкладеністю
public static <T extends Serializable> T deepClone(T object) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais)) {
return (T) ois.readObject();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// Або через Jackson (JSON)
public static <T> T cloneViaJson(T object, Class<T> clazz) {
String json = objectMapper.writeValueAsString(object);
return objectMapper.readValue(json, clazz);
}
Типові помилки
- Поверхнева копія мутабельних полів
// ❌ Зміна копії впливає на оригінал copy.getList().add("item"); original.getList().contains("item"); // true! - Prototype у Singleton
// ❌ Prototype створиться один раз @Service public class MyService { @Autowired private PrototypeBean bean; // Один екземпляр! }
🔴 Senior Level
Проблеми Cloneable на рівні JMM
// super.clone() працює на рівні Object.clone()
// → Копіює пам'ять, оминаючи конструктори
// → final поля НЕ ініціалізуються
// → Порушується інваріант класу
public class ImmutableUser {
private final String name;
private final List<String> permissions;
public ImmutableUser(String name, List<String> permissions) {
this.name = name;
this.permissions = List.copyOf(permissions);
}
// ❌ clone() не може ініціалізувати final поля!
// Доведеться використовувати reflection або unsafe
}
Copy Constructor vs Cloneable: Benchmark
1M копіювань:
Constructor copy: 50ms (прямий виклик)
clone(): 80ms (native-метод, НЕ використовує reflection. Але обходить конструктор, тому інваріанти класу можуть порушитися)
JSON clone: 500ms (серіалізація)
Serialization: 800ms (Java serialization)
Spring Prototype Lifecycle
// Spring НЕ керує lifecycle Prototype бінів!
@Component
@Scope("prototype")
public class ExpensiveResource {
@PostConstruct
public void init() {
System.out.println("Created"); // Викличеться
}
@PreDestroy
public void cleanup() {
System.out.println("Destroyed"); // НЕ викличеться! ❌
}
}
// Рішення: вручну очистити
@Component
public class ResourceCleaner {
private final List<ExpensiveResource> resources = new ArrayList<>();
public ExpensiveResource getResource() {
ExpensiveResource r = context.getBean(ExpensiveResource.class);
resources.add(r);
return r;
}
@PreDestroy
public void cleanup() {
resources.forEach(ExpensiveResource::cleanup);
}
}
Prototype для оптимізації
// Кешований прототип замість дорогого створення
public class DocumentTemplate {
private static final Document PROTOTYPE;
static {
// Дорога ініціалізація (БД, мережа)
PROTOTYPE = loadFromDatabase();
}
public static Document createDocument() {
return new Document(PROTOTYPE); // Копіюємо прототип
}
}
Production Experience
Реальний сценарій: Prototype + Singleton баг
- OrderProcessor (prototype) внедрений в OrderService (singleton)
- Баг: всі замовлення оброблялись одним процесором
- 4 години дебагу → знайшли через thread dump
- Рішення: ObjectProvider
Best Practices
- НЕ використовуйте Cloneable — конструктор копіювання кращий
- Глибока копія для мутабельних полів
- @Scope(“prototype”) + ObjectProvider у Spring
- JSON клонування для складних об’єктів
- Прототип-кеш для дорогих об’єктів
- @PreDestroy НЕ працює для Prototype у Spring
- final поля несумісні з clone()
Резюме для Senior
- Cloneable = defects design, уникайте
- Copy Constructor = типобезпечний, працює з final
- Spring Prototype = новий бін при кожному getBean()
- Lifecycle: @PreDestroy НЕ викликається для Prototype
- Singleton + Prototype = пастка → ObjectProvider/@Lookup
- Deep Clone: JSON/Serialization для складних об’єктів
- Performance: constructor > clone() > JSON > serialization
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Prototype — створення об’єктів копіюванням наявного, а не через конструктор
- Cloneable у Java — поганий дизайн: маркерний інтерфейс без методу clone(), поверхнева копія, не працює з final
- Рекомендований підхід: конструктор копіювання (Copy Constructor) — типобезпечний, працює з final полями
- Глибока копія копіює всі мутабельні поля, поверхнева — копіює посилання (зміна впливає на оригінал)
- Spring @Scope(“prototype”) — новий екземпляр при кожному getBean(), але @PreDestroy НЕ викликається
- Prototype у Singleton — пастка: створиться один раз, рішення через ObjectProvider або @Lookup
- Benchmark: constructor copy (50ms) > clone() (80ms) > JSON (500ms) > serialization (800ms)
Часті уточнювальні запитання:
- Чому Cloneable поганий? — Маркерний інтерфейс, protected clone(), shallow copy за замовчуванням, несумісний з final
- Чим глибоке копіювання відрізняється від поверхневого? — Глибоке копіює всі вкладені об’єкти, поверхневе — лише посилання
- Що буде якщо Prototype бін внедрити у Singleton? — Створиться один раз при ініціалізації Singleton
- Як зробити глибоку копію складного об’єкта? — Серіалізація (ByteArrayOutputStream) або JSON (Jackson)
Червоні прапорці (НЕ говорити):
- “Я використовую Cloneable — це стандартний Java підхід” — Joshua Bloch рекомендує Copy Constructor
- “Поверхнева копія достатня” — зміна копії впливає на оригінал для мутабельних полів
- “Prototype у Spring викликає @PreDestroy” — НЕ викликається для prototype бінів
- “Cloneable швидший за конструктор копіювання” — benchmark показує зворотне (80ms vs 50ms)
Пов’язані теми:
- [[8. Коли використовувати Builder]] — породжуючий патерн
- [[7. В чому різниця між Factory Method та Abstract Factory]] — породжуючі патерни
- [[3. Що таке Singleton]] — Singleton vs Prototype scopes
- [[2. Які категорії патернів існують]] — Creational патерни
- [[16. Які антипатерни ви знаєте]] — Copy-Paste Programming