🧩 Раздел 2 · Вопрос #9

Что такое 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);

Когда применять:

  1. Создание нового объекта требует тяжелых вычислений, чтения файловой системы или медленных сетевых запросов к БД.
  2. Система должна создавать множество похожих объектов, отличающихся лишь несколькими полями.
  3. Требуется изолировать клиентский код от конкретных классов создаваемых объектов (клиент знает только абстрактный интерфейс с методом 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:

  1. Отсутствие метода в интерфейсе: Cloneable — маркерный интерфейс без единого метода. Метод clone() объявлен в классе Object как protected. Реализация интерфейса Cloneable не обязывает компилятор проверять наличие публичного метода clone().
  2. Поверхностное копирование (Shallow Copy): super.clone() выполняет побитовое копирование полей памяти. Ссылочные типы (списки, массивы, вложенные объекты) копируются по ссылке:
    BrokenUser copy = original.clone();
    copy.getRoles().add("ROOT"); // ❌ Мутирует список и в объекте original!
    
  3. Несовместимость с final-полями: Метод clone() не может переприсвоить значения полям с модификатором final, так как они могут инициализироваться только в конструкторе.
  4. Обход конструктора: super.clone() создает объект в памяти напрямую через native-вызов JVM, полностью минуя конструкторы класса, что нарушает инварианты инициализации.
  5. 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)   │ (Полностью независимые копии)
└─────────────────┘         └───────────────┘

Способы реализации глубокого копирования:

  1. Ручное создание через конструкторы копирования (самый быстрый и безопасный способ).
  2. Сериализация через Jackson / Fastjson (JSON):
    public static <T> T deepCopyJson(T object, Class<T> clazz) {
        String json = objectMapper.writeValueAsString(object);
        return objectMapper.readValue(json, clazz);
    }
    
  3. 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; // ❌ Бин будет одним и тем же для всех заказов!
}

Корректные решения:

  1. Внедрение 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(); // Каждый раз новый экземпляр!
        }
    }
    
  2. Методная инъекция @Lookup: Spring переопределяет метод с помощью генерации CGLIB-прокси.

🔴 Senior Level

Внутреннее устройство Object.clone() в HotSpot JVM

На уровне JVM метод Object.clone() является native-методом, реализованным на C++:

  1. Метод выполняет проверку: если класс не реализует интерфейс Cloneable, немедленно выбрасывается CloneNotSupportedException.
  2. JVM выделяет непрерывный блок памяти в куче (Heap), точно соответствующий размеру оригинального объекта.
  3. Происходит прямое побайтовое копирование структуры заголовка (Mark Word + Klass Word) и полей данных.
  4. Конструктор <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 каверзных вопроса с ответами

  1. Почему super.clone() ломает инкапсуляцию и инварианты классов с final-полями?
    • Ответ: super.clone() выделяет память и побайтово копирует поля на уровне JVM без вызова конструктора. Если у класса есть final-поле с мутабельным объектом (например, final List<String> list), то в клонированном объекте это поле будет ссылаться на тот же самый список. Изменить ссылку на новый список невозможно, так как компилятор Java запрещает переприсваивание final-полей вне конструктора.
  2. Вызывается ли @PreDestroy для бинов со scope prototype в Spring?
    • Ответ: Нет, Spring передает prototype-бин клиенту и больше не отслеживает его жизненный цикл. Сборкой мусора и освобождением ресурсов должен заниматься вызывающий код (например, через реализацию кастомного BeanPostProcessor или явный вызов метода очистки).
  3. Как правильно внедрить Prototype-бин в Singleton-сервис?
    • Ответ: Прямое внедрение через @Autowired приведет к тому, что Prototype создастся лишь один раз при создании синглтона. Корректные решения: использовать ObjectProvider<T>.getObject(), методную инъекцию @Lookup, либо внедрять фабрику Provider<T> / ApplicationContext.
  4. В чем разница между поверхностным (shallow) и глубоким (deep) клонированием?
    • Ответ: При поверхностном клонировании копируются только примитивные типы и ссылки на объекты (ссылочные поля оригинала и копии указывают на одну и ту же область памяти). При глубоком клонировании рекурсивно создаются дубликаты всех вложенных объектов, гарантируя полную независимость копии от оригинала.

Типичные ошибки и Red Flags

  • 🚩 Использование implements Cloneable в новом коде вместо конструкторов копирования.
  • 🚩 Поверхностное копирование коллекций, приводящее к скрытым мутациям данных в параллельных потоках.
  • 🚩 Ожидание, что Spring закроет соединения или ресурсы prototype-бина через аннотацию @PreDestroy.
  • 🚩 Прямой @Autowired prototype-бина в singleton-бин с предположением, что для каждого HTTP-запроса будет создаваться новый экземпляр.

Связанные темы