Что такое Prototype pattern?
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