You have a sample order with the shipping address and product list already configured, and you need thousands of copies with a few fields changed for each customer. Rewriting the whole initialization is tedious, while calling clone() invites shallow-copy bugs where editing the copy corrupts the original. The Prototype pattern solves the problem of duplicating objects without making your code depend on their concrete classes. This article walks through the Prototype pattern in Java, from the JDK's Cloneable mechanism and why Joshua Bloch advises avoiding it, to copy constructors, copy factories, a prototype registry and deep copies via serialization. Every example targets Java 17 (compiled with javac --release 17, using only APIs available in Java 17), was actually run, and the output is pasted verbatim.
The Prototype pattern belongs to the creational group, right after Builder pattern: from Effective Java to Lombok and records. Builder constructs an object from scratch, while Prototype duplicates an object that already exists. To review how to separate object creation from logic, see Factory Method and Abstract Factory: separating object creation from logic; and for Singleton, a pattern that clone() can easily break, see Singleton pattern in Java: lazy init, holder, enum and the Java Memory Model.
What the Prototype pattern is and what problem it solves
According to the Refactoring.Guru description of Prototype, it is a creational pattern that lets you copy existing objects without making your code dependent on their concrete classes. The copying is delegated to the objects being cloned. The problem it names is very concrete: from the outside, you cannot copy an object by creating a new one and copying every field, because some fields are private, and because you would need to know its concrete class while your code only holds an interface-typed reference.
| Role | Responsibility | Example in this article |
|---|---|---|
| Prototype | Declares the cloning method | Mau.Prototype<T> (or Cloneable) |
| Concrete Prototype | Implements cloning and handles nested references | Email, DonDeep |
| Client | Clones through the interface without knowing the concrete class | Main |
| Registry (optional) | A store of named, ready-made templates | Mau.dangKy / Mau.lay |
The documented advantages: you clone without coupling to concrete classes, you cut repeated initialization code, and it is an alternative to inheritance when you need many preconfigured templates. The documented drawback is stated plainly: cloning objects with circular references is very hard. The hard part is how Java implements this idea.
The Prototype pattern with clone() and Cloneable: shallow copy by default
Java ships a built-in mechanism for the Prototype pattern: Object.clone() and the Cloneable marker interface. The Javadoc for Object.clone() in Java SE 17 is explicit about three things. First, if the class does not implement Cloneable, clone() throws CloneNotSupportedException. Second, the default implementation creates a new object of the same class and assigns each field as a plain assignment; the contents of fields are not cloned, so it is a shallow copy. Third, by convention the returned object should be obtained from super.clone(), and the copy should be independent of the original.
The example below is the classic "accident" of the Prototype pattern in Java. DonShallow holds a (mutable) DiaChi and a List; its clone() only calls super.clone().
public class DonShallow implements Cloneable {
String ma; DiaChi giao; List<String> sanPham = new ArrayList<>();
@Override public DonShallow clone() {
try { return (DonShallow) super.clone(); }
catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}
}
DonShallow a = new DonShallow("D1", new DiaChi("12 Oak Street", "Austin"));
a.sanPham.add("Phone case");
DonShallow b = a.clone();
b.ma = "D2"; // String: reassigned, no side effect
b.giao.setDuong("99 Elm Street"); // shared mutable -> original changes too
b.sanPham.add("20W charger"); // shared list -> original changes too--- 1. Default clone() = shallow copy ---
original: D1 | 99 Elm Street, Austin | [Phone case, 20W charger]
copy: D2 | 99 Elm Street, Austin | [Phone case, 20W charger]
same DiaChi? true, same List? trueOnly the copy was edited, yet the original also changed its address and gained "20W charger". The reason: both objects point to one DiaChi and one List. ma is unaffected because String is immutable and we merely reassigned the reference. This is exactly what the Javadoc warns about: for mutable fields, you must replace the reference with a copy.
The correct Prototype pattern convention: deep copy with clone()
To get the Prototype pattern right, the fix is to call super.clone() and then deep-copy each mutable field. Every nested component must itself be able to clone:
@Override public DonDeep clone() {
try {
DonDeep c = (DonDeep) super.clone();
c.giao = this.giao.clone(); // DiaChi also implements Cloneable
c.sanPham = new ArrayList<>(this.sanPham); // String elements are immutable, so this is enough
return c;
} catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}--- 2. Convention-compliant deep clone() ---
original: D1 | 12 Oak Street, Austin | [Phone case]
copy: D2 | 99 Elm Street, Austin | [Phone case, 20W charger]This time the original is untouched. But look at the price: you must remember to copy every mutable field, and each new field is another chance to forget one. The covariant return type (DonDeep instead of Object) and widening clone() to public are necessary tricks because the original in Object is protected. The try/catch that throws AssertionError is boilerplate, since the checked exception can never happen once the class implements Cloneable.
Why Effective Java advises against Cloneable
In Effective Java, 3rd Edition, Item 13 "Override clone judiciously" boils down to this: the Cloneable mechanism is fragile, and you should use a copy constructor or copy factory instead, keeping clone() only for arrays. The reasons can be verified for yourself:
- Cloneable is a marker interface with no methods: it implicitly changes the behavior of the (protected)
Object.clone(), contrary to what an interface is normally for. - It bypasses constructors:
clone()creates an object without calling a constructor, so invariants established in the constructor can be skipped. - It conflicts with
finalfields: you cannot reassign a final field inclone()to swap in a deep copy. - Annoying checked exceptions and manual casts everywhere.
- Fragile under inheritance: if a superclass does not use
super.clone(), a subclass cannot get it right; ifclone()calls an overridable method, that method runs on a half-built copy.
The most common mistake beginners make when writing clone(): forgetting to declare implements Cloneable while still calling super.clone().
--- 5. Forgetting Cloneable ---
CloneNotSupportedException: vn.sudo.prototype.KhongCloneableThis error only surfaces at runtime, because Cloneable has no methods for the compiler to check.
Replacing the Prototype pattern with a copy constructor and copy factory
The alternative to clone()-style Prototype pattern that Bloch recommends is a copy constructor (which takes an object of the same class) or a copy factory (a similar static factory). Both go through a constructor, so fields can be final, invariants are checked, and there are no casts or exceptions to catch.
public final class DonCopy {
private final String ma;
private final DiaChi giao;
private final List<String> sanPham;
public DonCopy(String ma, DiaChi giao, List<String> sanPham) {
this.ma = ma; this.giao = new DiaChi(giao); this.sanPham = new ArrayList<>(sanPham);
}
public DonCopy(DonCopy goc) { this(goc.ma, goc.giao, goc.sanPham); } // copy constructor
public static DonCopy of(DonCopy goc) { return new DonCopy(goc); } // copy factory
public DonCopy voiMa(String maMoi) { return new DonCopy(maMoi, giao, sanPham); } // copy with a change
}--- 3. Copy constructor / copy factory ---
original: D1 | 12 Oak Street, Austin | [Phone case]
copy: D2 | 99 Elm Street, Austin | [Phone case, 20W charger]The drawback of a copy constructor compared to clone(): it is fixed to the static type. If the client only holds a superclass reference, the superclass's copy constructor will "slice" the object down to the superclass, while clone() still preserves the real type. This is a genuine strength of clone(), and the core reason the Prototype pattern clones through an interface:
Hinh h = new Hinh.Tron("red", 2.5);
Hinh qua_clone = h.clone();
Hinh qua_copy = new Hinh(h); // copy constructor by static type Hinh--- 4. Inheritance trap ---
clone() -> Tron(red, r=2.5) [Tron]
new Hinh(h) -> Hinh(red) [Hinh]The copy made through the copy constructor loses the radius and even the Tron type. So when you need polymorphic cloning, do not mechanically abandon clone(): either define your own nhanBan() method on your own interface (true to the spirit of Prototype), or use the Prototype pattern with an explicit interface as in the next section.
The Prototype pattern with a custom interface and a Prototype Registry
Instead of depending on Cloneable, many codebases apply the Prototype pattern by defining their own Prototype<T> interface with a nhanBan() method. The benefits: a correct return type, no checked exceptions, and it forces whoever writes the class to think about deep copying. Combined with a registry (a store of named, ready-made templates, as the GoF book describes) you get a way to create preconfigured objects without knowing the concrete class:
public interface Prototype<T> { T nhanBan(); }
public static class Email implements Prototype<Email>, Serializable {
String tieuDe; List<String> nguoiNhan = new ArrayList<>(); Map<String, String> bien = new HashMap<>();
@Override public Email nhanBan() {
Email c = new Email(tieuDe);
c.nguoiNhan.addAll(nguoiNhan);
c.bien.putAll(bien);
return c;
}
}
Mau kho = new Mau();
kho.dangKy("welcome", chao);
Mau.Email e1 = kho.lay("welcome"); e1.nguoiNhan.add("lan@example.com");
Mau.Email e2 = kho.lay("welcome"); e2.bien.put("name", "Minh");--- 6. Prototype registry ---
template: Welcome new customer [support@example.com] {name={{name}}}
e1 : Welcome new customer [support@example.com, lan@example.com] {name={{name}}}
e2 : Welcome new customer [support@example.com] {name=Minh}The template stays intact after two clones, and the two copies are independent of each other. This is a sensible way to use the Prototype pattern for email templates, report templates, per-customer default configurations, or sample orders for test environments. One caution: the registry should return a copy (lay calls nhanBan()) and never the template itself, otherwise a client can modify the shared template.
Deep copy via serialization and its limits
For complex object graphs, hand-writing every cloning step is error-prone. An old trick is deep copying through serialization: write the object to a byte array and read it back.
public static <T extends Serializable> T deepCopyBangSerialization(T goc) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bos)) { out.writeObject(goc); }
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
return (T) in.readObject();
}
}--- 7. Deep copy via serialization ---
template: [support@example.com] | copy: [support@example.com, other@example.com] | different object: trueThis approach handles circular references on its own, but it has a cost: every class in the graph must be Serializable, transient fields are dropped, it is much slower than direct copying (rough measurements below), and it is dangerous for security if you read untrusted data. Use it only for internal tooling or test code, and never deserialize data that comes from outside. Third-party libraries such as Jackson (convert to JSON and back) or MapStruct allow deep copies without Serializable, but they carry their own costs.
To have numbers instead of gut feeling, the code in code/s6-05/src/vn/sudo/prototype/Bench.java clones 100,000 template emails two ways, after 20,000 warm-up iterations, on JDK 21 (Apple M4). This is a rough measurement with System.nanoTime, not JMH, so it is only valid as an order of magnitude:
run 1: nhanBan() 13 ms | serialization 616 ms (100000 copies)
run 2: nhanBan() 3 ms | serialization 492 ms (100000 copies)
run 3: nhanBan() 2 ms | serialization 533 ms (100000 copies)For a small object like this, serialization is roughly two hundred times slower or more. When you apply the Prototype pattern on a system's hot path, prefer a copy constructor or a hand-written nhanBan() method.
The Prototype pattern inside the JDK itself
The JDK applies the Prototype pattern both ways: arrays have a clone() that returns the right type, the legacy collections have a shallow clone(), and List.copyOf (Java 10) is a modern-style copy factory. Here is the output of this Prototype pattern example:
--- 8. Arrays and collections in the JDK ---
array clone: [1, 2, 3] / [99, 2, 3]
ArrayList.clone is shallow: original=CHANGED, Austin, same element=true
List.copyOf: 1 element (original 2), element still shared: trueThe last two lines are the important lesson: even the JDK only makes shallow copies. ArrayList.clone() and List.copyOf create a new list, but the elements are still the same objects, so editing a DiaChi in the copy still changes the original. Do not assume "copy" means "fully independent" when the elements are still mutable. List.copyOf is only safe when elements are immutable (String, immutable records, Integer).
When not to use the Prototype pattern
The Prototype pattern is not always worth it. Think carefully, or choose another approach, when:
- The object is immutable: the Prototype pattern is unnecessary, there is no need to clone, and sharing one reference is enough; if you need a modified copy, use a "with-er" or
toBuilder()as in the Builder article. - The object holds external resources: DB connections, sockets, file handles, streams. Cloning them is usually meaningless or dangerous.
- The graph has circular references: Refactoring.Guru calls this the hard part; you need a map of already-copied objects, or serialization.
- Initialization is cheap anyway: if
newis fast enough, the Prototype pattern only makes the code harder to read. - Singleton: if a Singleton class implements
Cloneable, uniqueness can be broken; blockclone()by throwing an exception.
Common mistakes
- Calling
clone()in the Prototype pattern and assuming it is a deep copy: the default is always shallow. Always check mutable fields (the example above shows the consequences). - Forgetting
implements Cloneable: the error only surfaces at runtime asCloneNotSupportedException. - Not using
super.clone(): building withnewinsideclone()breaks subclasses because the copy has the wrong type. - Copying a collection but not its mutable elements:
new ArrayList<>(ds)andList.copyOfboth copy elements shallowly. - A registry that returns the template itself: clients modify the shared template; always return a copy.
- A copy constructor that loses the subclass type: see
Hinhabove; chooseclone()ornhanBan()when you need polymorphism. - Deserializing untrusted data: use serialization only to copy your own objects within the same process.
Checklist
- Use the Prototype pattern when you need to clone objects through an interface without knowing the concrete class, or when a preconfigured template is more expensive to build than to clone.
- With the Prototype pattern, prefer a copy constructor or copy factory by default; fields can be
final. - If you must use
clone():implements Cloneable, callsuper.clone(), deep-copy every mutable field, and return a covariant type. - When you need polymorphic cloning, define your own
Prototype<T>interface. - A registry always returns a copy; document whether the copy is shallow or deep.
- Use serialization only for complex graphs and trusted data.
Environment for running the Prototype pattern examples: the source targets Java 17 (javac --release 17, no APIs after Java 17). The full source and run.sh are in code/s6-05.
Next article
Adapter pattern: connecting legacy APIs to new interfaces. After the creational group, the series moves on to the structural group: how to wrap an incompatible API so it can be used in existing code without modifying the original source.



