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

Что такое Prototype pattern?

4. Не работает с final полями 5. Checked exception CloneNotSupportedException

Версии по языкам: English Russian Ukrainian

🟢 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:

  1. Не содержит метода clone() (маркерный интерфейс)
  2. clone() защищённый — нужно переопределять
  3. Поверхностная копия по умолчанию
  4. Не работает с final полями
  5. 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);
}

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

  1. Поверхностная копия мутабельных полей
    // ❌ Изменение копии влияет на оригинал
    copy.getList().add("item");
    original.getList().contains("item"); // true!
    
  2. 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

  1. НЕ используйте Cloneable — конструктор копирования лучше
  2. Глубокая копия для мутабельных полей
  3. @Scope(“prototype”) + ObjectProvider в Spring
  4. JSON клонирование для сложных объектов
  5. Прототип-кэш для дорогих объектов
  6. @PreDestroy НЕ работает для Prototype в Spring
  7. 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