Back to Creational: Singleton, Factory, Builder, Prototype
Q3Question 3 of 12

Prototype pattern: the shallow-clone trap, and why a copy constructor beats Cloneable (Bloch's criticism)

Key Concepts
  • Prototype creates new objects by copying an existing instance instead of constructing from scratch.
  • Shallow copy duplicates the top-level object but shares references to inner mutable objects – mutating the copy mutates the original.
  • Deep copy recursively copies inner mutable objects so the copy is fully independent.
  • Cloneable is a marker interface with no clone method, bypasses constructors, cannot assign final fields, and produces shallow copies by default.
  • Joshua Bloch recommends a copy constructor or copy factory: explicit, type-safe, works with final fields, no checked exceptions.
  • For arrays, clone is fine and idiomatic; for immutable objects, copying is unnecessary.

Definition

Prototype copies an existing object to make a new one; the shallow-clone trap is when the copy still shares the original's inner mutable objects, and a copy constructor avoids the mess of Cloneable.
  • Prototype pattern – creates new objects by copying an existing instance (the prototype) rather than constructing from scratch.
  • Shallow copy – duplicates the top-level object but copies only the references of its fields, not the objects they point to.
  • Deep copy – recursively copies inner mutable objects so the copy is fully independent.
  • Cloneable – a marker interface with no clone method; uses Object.clone() which is shallow by default and bypasses constructors.
  • Copy constructor – a constructor that takes an instance of the same class and copies its state explicitly; recommended by Joshua Bloch.

Imagine National Bank of India has a standard loan-application template pre-filled with the bank's default terms. When Rajesh applies, the clerk photocopies the template and just changes the applicant details. But if the template has a stapled booklet of sub-documents and the photocopy only copies the cover page while still pointing to the same shared booklet, then editing Rajesh's booklet accidentally changes Priya's too. That shared-booklet problem is the shallow-clone trap.

Understanding the Concepts

What is the Prototype pattern?

Prototype answers "I need another object just like this one." Rather than calling a complicated constructor and reconfiguring everything, you copy a prototype that is already set up.

JAVA
package com.bank.model;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class Account {
    private String accountId;
    private BigDecimal balance;
    private List<String> transactionIds;

    public Account(String accountId, BigDecimal balance, List<String> transactionIds) {
        this.accountId = accountId;
        this.balance = balance;
        this.transactionIds = new ArrayList<>(transactionIds);
    }

    // Copy constructor – deep copy
    public Account(Account other) {
        this.accountId = other.accountId;
        this.balance = other.balance;
        this.transactionIds = new ArrayList<>(other.transactionIds);
    }

    public void addTransaction(String txnId) {
        transactionIds.add(txnId);
    }

    public List<String> getTransactionIds() {
        return List.copyOf(transactionIds);
    }

    @Override
    public String toString() {
        return accountId + " bal=" + balance + " txns=" + transactionIds;
    }
}

What is the shallow-clone trap?

A shallow copy duplicates the top-level object but copies only the references. If an Account holds a List, a shallow copy gives you a new Account whose list field points to the very same list.

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class ShallowCloneTrap {
    public static void main(String[] args) {
        List<String> txns = new ArrayList<>(List.of("TXN-001"));

        // Simulate shallow copy (just copy references)
        Account original = new Account("ACC-001", new BigDecimal("100000"), txns);

        // SHALLOW: shares the same list!
        Account shallowCopy = new Account("ACC-001", original.balance, original.transactionIds);
        // If transactionIds were the SAME reference:
        // shallowCopy.addTransaction("TXN-HACK");
        // original would also show TXN-HACK!

        // DEEP COPY: uses copy constructor
        Account deepCopy = new Account(original);
        deepCopy.addTransaction("TXN-002");

        System.out.println("Original: " + original);
        System.out.println("Deep copy: " + deepCopy);
    }
}
TEXT
Original: ACC-001 bal=100000 txns=[TXN-001]
Deep copy: ACC-001 bal=100000 txns=[TXN-001, TXN-002]

The deep copy has its own independent list. The original is untouched.

Why is Cloneable problematic?

Joshua Bloch, in Effective Java, criticises Cloneable harshly:

  • Cloneable is a marker interface with no clone method – it merely changes the behaviour of Object.clone(), which is confusing.
  • clone() is declared on Object as protected and returns Object, so callers need casts.
  • clone() does not run constructors, so it can produce an object in an invalid state.
  • clone() cannot assign final fields, which conflicts with immutability.
  • It throws the checked CloneNotSupportedException even when cloning is valid.
  • The contract between clone, super.clone, and deep copying is fragile.
JAVA
package com.bank.model;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

// PROBLEMATIC: using Cloneable
public class AccountCloneable implements Cloneable {
    private String accountId;
    private BigDecimal balance;
    private List<String> transactionIds;

    public AccountCloneable(String id, BigDecimal bal, List<String> txns) {
        this.accountId = id;
        this.balance = bal;
        this.transactionIds = new ArrayList<>(txns);
    }

    @Override
    public AccountCloneable clone() {
        try {
            AccountCloneable copy = (AccountCloneable) super.clone();
            // MUST manually deep-copy mutable fields!
            copy.transactionIds = new ArrayList<>(this.transactionIds);
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError("Should not happen", e);
        }
    }
}

Why is a copy constructor preferred?

A copy constructor is explicit, type-safe, respects final fields, and you control exactly what is copied.

JAVA
package com.bank.model;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class AccountSafe {
    private final String accountId;
    private final BigDecimal balance;
    private final List<String> transactionIds;

    public AccountSafe(String id, BigDecimal balance, List<String> txns) {
        this.accountId = id;
        this.balance = balance;
        this.transactionIds = List.copyOf(txns);
    }

    // Copy constructor: explicit, works with final fields
    public AccountSafe(AccountSafe other) {
        this.accountId = other.accountId;
        this.balance = other.balance;
        this.transactionIds = List.copyOf(other.transactionIds);
    }

    // Static copy factory (alternative form)
    public static AccountSafe copyOf(AccountSafe other) {
        return new AccountSafe(other);
    }

    @Override
    public String toString() {
        return accountId + " bal=" + balance + " txns=" + transactionIds;
    }
}
  • Benefits over Cloneable:
  • Runs a real constructor, so the object is always valid.
  • Works with final fields.
  • No casts, no checked exceptions.
  • Deep-copy logic is explicit and visible.

When is clone acceptable?

For arrays, clone() is fine and idiomatic because arrays have a well-defined, efficient clone:

JAVA
int[] original = {1, 2, 3, 4, 5};
int[] copy = original.clone(); // perfectly fine for arrays

For genuinely immutable classes, copying is unnecessary: you can safely share the same instance since nothing can mutate it.

What are the use cases for Prototype?

  • Expensive construction: building an object requires a database call or heavy computation; cloning avoids repeating that cost.
  • Pre-configured templates: keep a prototype with defaults and clone it for each use.
  • Runtime type selection: copy an object of unknown concrete type without knowing the type statically.
  • Snapshotting: clone captures the current state for what-if scenarios.
JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.List;

public class PrototypeDemo {
    public static void main(String[] args) {
        // Template: pre-configured order
        AccountSafe template = new AccountSafe("TEMPLATE", new BigDecimal("0"), List.of("SETUP"));

        // Prototype: copy and customise
        AccountSafe rajesh = new AccountSafe(template);
        AccountSafe priya = AccountSafe.copyOf(template);

        System.out.println("Template: " + template);
        System.out.println("Rajesh: " + rajesh);
        System.out.println("Priya: " + priya);
    }
}
TEXT
Template: TEMPLATE bal=0 txns=[SETUP]
Rajesh: TEMPLATE bal=0 txns=[SETUP]
Priya: TEMPLATE bal=0 txns=[SETUP]

How do you test that deep copy is correct?

A proper deep-copy test verifies that modifying the copy does not affect the original. This is the definitive test for the shallow-clone trap: mutate every mutable field in the copy and assert the original is unchanged.

JAVA
package com.bank.model;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class DeepCopyTest {
    static class AccountTemplate {
        String type;
        BigDecimal balance;
        List<String> features;

        AccountTemplate(String type, BigDecimal balance, List<String> features) {
            this.type = type;
            this.balance = balance;
            this.features = features;
        }

        // Copy constructor (deep copy)
        AccountTemplate(AccountTemplate source) {
            this.type = source.type;
            this.balance = source.balance;
            this.features = new ArrayList<>(source.features);
        }
    }

    public static void main(String[] args) {
        AccountTemplate original = new AccountTemplate(
            "SAVINGS", new BigDecimal("50000"), new ArrayList<>(List.of("ATM", "UPI")));
        AccountTemplate copy = new AccountTemplate(original);

        // Mutate the copy
        copy.features.add("CHEQUE");
        copy.type = "CURRENT";

        // Original must be unaffected
        System.out.println("Original features: " + original.features);
        System.out.println("Copy features: " + copy.features);
        System.out.println("Deep copy correct: " + !original.features.contains("CHEQUE"));
    }
}
TEXT
Expected output:
Original features: [ATM, UPI]
Copy features: [ATM, UPI, CHEQUE]
Deep copy correct: true

What should a senior engineer know?

  • The most common mistake is a shallow copy that accidentally shares mutable state.
  • In modern Java, records with a compact copy helper reduce the need for manual clone logic.
  • Bloch's criticism of Cloneable is a standard interview topic; know the five problems.
  • For banking what-if scenarios, deep-copy an account to simulate transactions without touching the real one.

Visual Overview

DIAGRAM
  Shallow vs Deep Copy
  ─────────────────────

  SHALLOW COPY:
  ┌──────────────┐     ┌──────────────┐
  │ Original     │     │ Shallow Copy │
  │ accountId    │     │ accountId    │
  │ balance      │     │ balance      │
  │ txnIds ──────┼──┐  │ txnIds ──────┼──┐
  └──────────────┘  │  └──────────────┘  │
                    │                    │
                    └──── SAME LIST ─────┘
                    Mutating one mutates both!

  DEEP COPY:
  ┌──────────────┐     ┌──────────────┐
  │ Original     │     │ Deep Copy    │
  │ accountId    │     │ accountId    │
  │ balance      │     │ balance      │
  │ txnIds ──────┼──┐  │ txnIds ──────┼──┐
  └──────────────┘  │  └──────────────┘  │
                    ▼                    ▼
              [TXN-001]          [TXN-001]  (independent)

Key Takeaway

The Prototype pattern creates a new object by copying an existing, pre-configured instance rather than building from scratch. Rajesh uses it at National Bank of India for what-if scenarios: he deep-copies a real account to simulate hypothetical transactions without touching the real one.

The central danger is the shallow-clone trap. A shallow copy duplicates the top-level object but shares references to inner mutable objects, so if the account holds a list of transactions, the copy's list is the same list. Adding a transaction to the copy also adds it to the original. A deep copy fixes this by recursively copying inner mutable objects.

Joshua Bloch's criticism of Cloneable is standard interview material. Cloneable is a marker interface with no clone method, clone() bypasses constructors and cannot assign final fields, it throws a checked exception even when cloning is valid, and it produces a shallow copy by default. For all these reasons, Rajesh prefers a copy constructor: it is explicit, type-safe, works with final fields, and deep-copy logic is visible.

The senior nuances are that for plain arrays, clone() is fine and idiomatic, and for genuinely immutable classes copying is unnecessary because sharing one instance is safe when nothing can mutate it.

Remember one sentence: prefer a copy constructor over Cloneable because it is explicit, type-safe, works with final fields, and makes the deep-copy logic visible.

---

Share
Use ← → arrow keys to navigate