Питання 9 · Розділ 2

Що таке патерн Prototype?

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