Question 9 · Section 2

What is Prototype Pattern?

4. Doesn't work with final fields 5. Checked exception CloneNotSupportedException

Language versions: English Russian Ukrainian

Junior Level

Prototype is a pattern that allows creating new objects by copying an existing one. Copy constructor is the recommended Java alternative to Object.clone().

Simple analogy: Document photocopying. Instead of writing a document from scratch each time, you copy a ready-made template and change the needed fields.

Example:

// Copy constructor
public class User {
    private String name;
    private List<String> permissions;

    // Ordinary constructor
    public User(String name, List<String> permissions) {
        this.name = name;
        this.permissions = new ArrayList<>(permissions);
    }

    // Copy constructor (Prototype)
    public User(User other) {
        this.name = other.name;
        this.permissions = new ArrayList<>(other.permissions); // Copy the list!
    }
}

// Usage
User template = new User("admin", List.of("read", "write"));
User copy = new User(template);  // Copy of the template!

When to use:

  • Object creation is expensive (DB request, network)
  • Many similar objects are needed
  • Constructor is complex

Middle Level

Why Cloneable in Java is Bad

// Problems with Cloneable
public class User implements Cloneable {
    private String name;
    private List<String> permissions;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();  // Shallow copy!
    }
}

User original = new User("Ivan", new ArrayList<>(List.of("read")));
User copy = (User) original.clone();

// Shallow copy:
copy.getPermissions().add("admin");
// original.getPermissions() also changed!

Cloneable problems:

  1. Doesn’t contain a clone() method (marker interface)
  2. clone() is protected — needs overriding
  3. Shallow copy by default
  4. Doesn’t work with final fields
  5. Checked exception CloneNotSupportedException

Solution: Copy Constructor

// Recommended approach (Joshua Bloch)
public class User {
    private final String name;
    private final List<String> permissions;

    // Copy constructor
    public User(User other) {
        this.name = other.name;
        this.permissions = new ArrayList<>(other.permissions); // Deep copy!
    }

    // Static factory method
    public static User copyOf(User other) {
        return new User(other);
    }
}

User original = new User("Ivan", List.of("read"));
User copy = User.copyOf(original);  // Clear and type-safe

Deep vs Shallow Copy

// Shallow — copies references
public User(User other) {
    this.name = other.name;               // String — immutable, OK
    this.permissions = other.permissions; // Reference to the same list!
}

// Deep — copies everything
public User(User other) {
    this.name = other.name;
    this.permissions = new ArrayList<>(other.permissions); // New list
    this.address = new Address(other.address);             // Copy nested object
}

Prototype Scope in Spring

// Prototype = new instance on each request
@Component
@Scope("prototype")
public class OrderProcessor {
    private final Order order;

    public OrderProcessor(Order order) {
        this.order = order;  // New Order each time
    }
}

// Usage
OrderProcessor p1 = context.getBean(OrderProcessor.class);
OrderProcessor p2 = context.getBean(OrderProcessor.class);
// p1 != p2 -> different objects!

// Trap: Prototype inside Singleton
@Component  // Singleton!
public class OrderService {
    @Autowired
    private OrderProcessor processor;  // Created ONCE!
}

// Solution 1: @Lookup
@Component
public abstract class OrderService {
    @Lookup
    protected abstract OrderProcessor getProcessor();
}

// Solution 2: ObjectProvider
@Component
public class OrderService {
    private final ObjectProvider<OrderProcessor> processorProvider;

    public void process() {
        OrderProcessor processor = processorProvider.getObject();  // New each time!
    }
}

Cloning via Serialization

// For complex objects with deep nesting
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);
    }
}

// Or via Jackson (JSON)
public static <T> T cloneViaJson(T object, Class<T> clazz) {
    String json = objectMapper.writeValueAsString(object);
    return objectMapper.readValue(json, clazz);
}

Typical Mistakes

  1. Shallow copy of mutable fields
    // Changing the copy affects the original
    copy.getList().add("item");
    original.getList().contains("item"); // true!
    
  2. Prototype inside Singleton
    // Prototype will be created once
    @Service
    public class MyService {
        @Autowired
        private PrototypeBean bean;  // Single instance!
    }
    

Senior Level

Cloneable Problems at JMM Level

// super.clone() works at Object.clone() level
// -> Copies memory, bypassing constructors
// -> final fields are NOT initialized
// -> Class invariant is violated

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() cannot initialize final fields!
    // Would need to use reflection or unsafe
}

Copy Constructor vs Cloneable: Benchmark

1M copies:
  Constructor copy:   50ms   (direct call)
  clone():            80ms   (native method, doesn't use reflection. But bypasses constructor, so class invariants may be violated)
  JSON clone:         500ms  (serialization)
  Serialization:      800ms  (Java serialization)

Spring Prototype Lifecycle

// Spring does NOT manage Prototype bean lifecycle!
@Component
@Scope("prototype")
public class ExpensiveResource {

    @PostConstruct
    public void init() {
        System.out.println("Created");  // Will be called
    }

    @PreDestroy
    public void cleanup() {
        System.out.println("Destroyed");  // Will NOT be called!
    }
}

// Solution: manually clean up
@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 for Optimization

// Cached prototype instead of expensive creation
public class DocumentTemplate {
    private static final Document PROTOTYPE;

    static {
        // Expensive initialization (DB, network)
        PROTOTYPE = loadFromDatabase();
    }

    public static Document createDocument() {
        return new Document(PROTOTYPE);  // Copy the prototype
    }
}

Production Experience

Real scenario: Prototype + Singleton bug

  • OrderProcessor (prototype) injected into OrderService (singleton)
  • Bug: all orders processed by the same processor
  • 4 hours of debugging -> found via thread dump
  • Solution: ObjectProvider

Best Practices

  1. DON’T use Cloneable — copy constructor is better
  2. Deep copy for mutable fields
  3. @Scope(“prototype”) + ObjectProvider in Spring
  4. JSON cloning for complex objects
  5. Prototype cache for expensive objects
  6. @PreDestroy does NOT work for Prototype in Spring
  7. final fields are incompatible with clone()

Senior Summary

  • Cloneable = defective design, avoid it
  • Copy Constructor = type-safe, works with final
  • Spring Prototype = new bean on each getBean()
  • Lifecycle: @PreDestroy is NOT called for Prototype
  • Singleton + Prototype = trap -> ObjectProvider/@Lookup
  • Deep Clone: JSON/Serialization for complex objects
  • Performance: constructor > clone() > JSON > serialization

Interview Cheat Sheet

Must know:

  • Prototype — creating objects by copying existing ones, not via constructor
  • Cloneable in Java — bad design: marker interface without clone() method, shallow copy, incompatible with final
  • Recommended approach: copy constructor — type-safe, works with final fields
  • Deep copy copies all mutable fields, shallow copy copies references (changes affect the original)
  • Spring @Scope(“prototype”) — new instance on each getBean(), but @PreDestroy is NOT called
  • Prototype in Singleton — trap: created once, solution via ObjectProvider or @Lookup
  • Benchmark: constructor copy (50ms) > clone() (80ms) > JSON (500ms) > serialization (800ms)

Common follow-up questions:

  • Why is Cloneable bad? — Marker interface, protected clone(), shallow copy by default, incompatible with final
  • How does deep copying differ from shallow? — Deep copies all nested objects, shallow copies only references
  • What happens if Prototype bean is injected into Singleton? — Created once during Singleton initialization
  • How to deep copy a complex object? — Serialization (ByteArrayOutputStream) or JSON (Jackson)

Red flags (DO NOT say):

  • “I use Cloneable — it’s the standard Java approach” — Joshua Bloch recommends Copy Constructor
  • “Shallow copy is sufficient” — changing the copy affects the original for mutable fields
  • “Prototype in Spring calls @PreDestroy” — NOT called for prototype beans
  • “Cloneable is faster than copy constructor” — benchmark shows the opposite (80ms vs 50ms)

Related topics:

  • [[08. When to use Builder]] — creational pattern
  • [[07. Difference between Factory Method and Abstract Factory]] — creational patterns
  • [[03. What is Singleton]] — Singleton vs Prototype scopes
  • [[02. What pattern categories exist]] — Creational patterns
  • [[16. What anti-patterns do you know]] — Copy-Paste Programming