Что такое Prototype pattern
Ксерокс или изготовление ключа по слепку. Если требуется создать новый документ с десятками сложных таблиц и настроек, намного быстрее сделать копию уже настроенного образца (ша...
🟢 Junior Level
Prototype (Прототип) — это порождающий паттерн проектирования, который позволяет создавать новые объекты путем копирования (клонирования) уже существующего экземпляра (прототипа) вместо создания с нуля через оператор new.
Бытовая аналогия
Ксерокс или изготовление ключа по слепку. Если требуется создать новый документ с десятками сложных таблиц и настроек, намного быстрее сделать копию уже настроенного образца (шаблона) и изменить в ней пару полей (например, ФИО клиента), чем заново заполнять все данные с чистого листа.
Простейшая реализация: Конструктор копирования (Copy Constructor)
В языке Java общепринятым и наиболее безопасным способом реализации паттерна Prototype является конструктор копирования:
public class UserProfile {
private String username;
private String role;
private List<String> permissions;
// Обычный конструктор
public UserProfile(String username, String role, List<String> permissions) {
this.username = username;
this.role = role;
this.permissions = new ArrayList<>(permissions);
}
// Конструктор копирования (паттерн Prototype)
public UserProfile(UserProfile prototype) {
this.username = prototype.username;
this.role = prototype.role;
// Важно: создаем новый список для защиты от побочных эффектов (глубокая копия)
this.permissions = new ArrayList<>(prototype.permissions);
}
public static UserProfile copyOf(UserProfile other) {
return new UserProfile(other);
}
}
// Использование:
UserProfile adminTemplate = new UserProfile("admin_base", "ADMIN", List.of("READ", "WRITE", "DELETE"));
UserProfile newAdmin = UserProfile.copyOf(adminTemplate);
Когда применять:
- Создание нового объекта требует тяжелых вычислений, чтения файловой системы или медленных сетевых запросов к БД.
- Система должна создавать множество похожих объектов, отличающихся лишь несколькими полями.
- Требуется изолировать клиентский код от конкретных классов создаваемых объектов (клиент знает только абстрактный интерфейс с методом
clone()).
🟡 Middle Level
Почему Cloneable и Object.clone() в Java считаются неудачным дизайном?
В стандартной библиотеке Java есть интерфейс java.lang.Cloneable, однако признанные эксперты (включая Джошуа Блоха) считают его архитектурной ошибкой:
// ❌ Проблемный стандартный подход Java
public class BrokenUser implements Cloneable {
private String name;
private List<String> roles;
@Override
public BrokenUser clone() {
try {
return (BrokenUser) super.clone(); // ⚠️ Поверхностная копия по умолчанию!
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Фатальные недостатки Cloneable:
- Отсутствие метода в интерфейсе:
Cloneable— маркерный интерфейс без единого метода. Методclone()объявлен в классеObjectкакprotected. Реализация интерфейсаCloneableне обязывает компилятор проверять наличие публичного методаclone(). - Поверхностное копирование (Shallow Copy):
super.clone()выполняет побитовое копирование полей памяти. Ссылочные типы (списки, массивы, вложенные объекты) копируются по ссылке:BrokenUser copy = original.clone(); copy.getRoles().add("ROOT"); // ❌ Мутирует список и в объекте original! - Несовместимость с
final-полями: Методclone()не может переприсвоить значения полям с модификаторомfinal, так как они могут инициализироваться только в конструкторе. - Обход конструктора:
super.clone()создает объект в памяти напрямую через native-вызов JVM, полностью минуя конструкторы класса, что нарушает инварианты инициализации. - Checked Exception: Метод вынуждает обрабатывать или пробрасывать архаичный
CloneNotSupportedException.
Поверхностное копирование (Shallow) против Глубокого (Deep Copy)
Исходный объект:
┌─────────────────┐
│ UserProfile │
│ username: "Ann"│
│ address ───────┼────────┐
└─────────────────┘ │
▼
Поверхностная копия: ┌───────────────┐
┌─────────────────┐ │ Address │ (Оба объекта ссылаются
│ UserProfile │ │ city: "Riga" │ на ОДИН экземпляр в куче!)
│ username: "Bob"│ └───────────────┘
│ address ───────┼────────▲
└─────────────────┘ │
Глубокая копия (Deep Copy):
┌─────────────────┐ ┌───────────────┐
│ UserProfile (1) │ ──────> │ Address (1) │
└─────────────────┘ └───────────────┘
┌─────────────────┐ ┌───────────────┐
│ UserProfile (2) │ ──────> │ Address (2) │ (Полностью независимые копии)
└─────────────────┘ └───────────────┘
Способы реализации глубокого копирования:
- Ручное создание через конструкторы копирования (самый быстрый и безопасный способ).
- Сериализация через Jackson / Fastjson (JSON):
public static <T> T deepCopyJson(T object, Class<T> clazz) { String json = objectMapper.writeValueAsString(object); return objectMapper.readValue(json, clazz); } - Java Serialization (ByteArrayOutputStream / ObjectOutputStream): медленно, требует интерфейса
Serializable, создает проблемы безопасности.
Паттерн Prototype в Spring Framework: @Scope("prototype")
В Spring Boot скоуп prototype создает новый экземпляр бина при каждом запросе из контекста:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ReportGenerator {
// Создается заново при каждом вызове getBean()
}
Проблема «Prototype внутри Singleton»:
Если внедрить Prototype-бин в Singleton-сервис через @Autowired, Prototype-бин внедрится ровно один раз при старте приложения:
@Service // Singleton
public class OrderService {
@Autowired
private ReportGenerator generator; // ❌ Бин будет одним и тем же для всех заказов!
}
Корректные решения:
- Внедрение
ObjectProvider<T>:@Service public class OrderService { private final ObjectProvider<ReportGenerator> generatorProvider; public OrderService(ObjectProvider<ReportGenerator> provider) { this.generatorProvider = provider; } public void process() { ReportGenerator gen = generatorProvider.getObject(); // Каждый раз новый экземпляр! } } - Методная инъекция
@Lookup: Spring переопределяет метод с помощью генерации CGLIB-прокси.
🔴 Senior Level
Внутреннее устройство Object.clone() в HotSpot JVM
На уровне JVM метод Object.clone() является native-методом, реализованным на C++:
- Метод выполняет проверку: если класс не реализует интерфейс
Cloneable, немедленно выбрасываетсяCloneNotSupportedException. - JVM выделяет непрерывный блок памяти в куче (Heap), точно соответствующий размеру оригинального объекта.
- Происходит прямое побайтовое копирование структуры заголовка (Mark Word + Klass Word) и полей данных.
- Конструктор
<init>не вызывается. Это значит, что любые механизмы аудита, счетчики созданных экземпляров, регистрация слушателей или генерация уникальных UUID, находящиеся в конструкторе, не сработают.
Жизненный цикл бинов со scope prototype в Spring
Критическое отличие Prototype-бина от Singleton в Spring:
- Spring инициализирует Prototype: выполняются внедрение зависимостей, методы
@PostConstructиInitializingBean.afterPropertiesSet(), прогоняютсяBeanPostProcessor. - Spring НЕ уничтожает Prototype: контейнер полностью отдает управление бином вызывающему коду и забывает о нем.
- Хук
@PreDestroyиDisposableBean.destroy()для prototype-бинов НИКОГДА не вызываются контейнером. - Если prototype-бин захватывает системные ресурсы (файловые дескрипторы, сокеты, соединения), их закрытие обязан выполнять клиентский код, иначе произойдет утечка ресурсов.
Бенчмарки производительности способов клонирования
| Метод клонирования | Относительная скорость | Вызов конструктора | Поддержка final |
Безопасность типов |
|---|---|---|---|---|
| Copy Constructor | 🟢 1x (наивысшая, ~15–20 ns) | ✅ Да | ✅ Да | ✅ Compile-time |
| Object.clone() | 🟡 ~1.5x (быстро, но небезопасно) | ❌ Нет (native memcpy) | ❌ Проблемы | ⚠️ Требует каста |
| JSON (Jackson Afterburner) | 🔴 ~20x–30x медленнее | ✅ Да | ⚠️ Требует геттеры | ⚠️ Runtime parsing |
| Java Serialization | 🔴 ~40x–50x медленнее | ❌ Нет | ⚠️ Сложно | ❌ Уязвимости OOS |
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Паттерн Prototype (Прототип) решает задачу создания новых объектов путем клонирования существующего прототипа, что особенно полезно при дорогой инициализации или сложной конфигурации. В Java стандартный интерфейс
Cloneableпризнан неудачным проектным решением из-за обхода конструктора, поверхностного копирования и несовместимости сfinal-полями. Эталонным способом реализации Prototype в Java является конструктор копирования (Copy Constructor) или статический фабричный методcopyOf(). В Spring Framework паттерн представлен областью видимости@Scope("prototype"), при этом важно помнить, что хук@PreDestroyдля таких бинов контейнером не вызывается».
3–4 каверзных вопроса с ответами
- Почему
super.clone()ломает инкапсуляцию и инварианты классов сfinal-полями?- Ответ:
super.clone()выделяет память и побайтово копирует поля на уровне JVM без вызова конструктора. Если у класса естьfinal-поле с мутабельным объектом (например,final List<String> list), то в клонированном объекте это поле будет ссылаться на тот же самый список. Изменить ссылку на новый список невозможно, так как компилятор Java запрещает переприсваиваниеfinal-полей вне конструктора.
- Ответ:
- Вызывается ли
@PreDestroyдля бинов со scopeprototypeв Spring?- Ответ: Нет, Spring передает prototype-бин клиенту и больше не отслеживает его жизненный цикл. Сборкой мусора и освобождением ресурсов должен заниматься вызывающий код (например, через реализацию кастомного
BeanPostProcessorили явный вызов метода очистки).
- Ответ: Нет, Spring передает prototype-бин клиенту и больше не отслеживает его жизненный цикл. Сборкой мусора и освобождением ресурсов должен заниматься вызывающий код (например, через реализацию кастомного
- Как правильно внедрить Prototype-бин в Singleton-сервис?
- Ответ: Прямое внедрение через
@Autowiredприведет к тому, что Prototype создастся лишь один раз при создании синглтона. Корректные решения: использоватьObjectProvider<T>.getObject(), методную инъекцию@Lookup, либо внедрять фабрикуProvider<T>/ApplicationContext.
- Ответ: Прямое внедрение через
- В чем разница между поверхностным (shallow) и глубоким (deep) клонированием?
- Ответ: При поверхностном клонировании копируются только примитивные типы и ссылки на объекты (ссылочные поля оригинала и копии указывают на одну и ту же область памяти). При глубоком клонировании рекурсивно создаются дубликаты всех вложенных объектов, гарантируя полную независимость копии от оригинала.
Типичные ошибки и Red Flags
- 🚩 Использование
implements Cloneableв новом коде вместо конструкторов копирования. - 🚩 Поверхностное копирование коллекций, приводящее к скрытым мутациям данных в параллельных потоках.
- 🚩 Ожидание, что Spring закроет соединения или ресурсы prototype-бина через аннотацию
@PreDestroy. - 🚩 Прямой
@Autowiredprototype-бина в singleton-бин с предположением, что для каждого HTTP-запроса будет создаваться новый экземпляр.
Связанные темы
- Когда использовать Builder — альтернативный порождающий паттерн
- В чём разница между Factory Method и Abstract Factory — порождающие фабрики
- Что такое Singleton — сравнение Singleton и Prototype
- Что такое иммутабельность — неизменяемые объекты и методы копирования
withX() - В чём разница между singleton и prototype scope — детали реализации в Spring