Prototype creates new objects by copying an existing instance instead of constructing from scratch. In Java, the language-blessed mechanism — Cloneable + Object.clone() — is broken (Bloch Item 13: "an extralinguistic mechanism"), so the modern toolkit is four alternatives: copy constructors, copy factories, manual deep copy, and serialization-based deep copy. Records collapse the easy case to almost nothing.
Why clone() is considered broken
Brief rundown of the sins (full treatment in topic 07's q05):
Cloneableis a marker interface that doesn't declareclone()—Object.clone()isprotected, you have to override and re-publish.clone()doesn't call any constructor — fields initialized by constructors are bypassed.- The default behavior is a shallow copy — mutable fields are shared, defeating the point.
- It interacts terribly with
finalfields — you can't assignfinalinclone(). - The return type was
Objectuntil Java 5 (covariant returns rescued it slightly).
Alternative 1 — Copy constructor
public final class Customer {
private final String name;
private final List<Address> addresses;
public Customer(String name, List<Address> addresses) {
this.name = name;
this.addresses = List.copyOf(addresses);
}
// copy constructor — explicit, no marker interfaces
public Customer(Customer other) {
this.name = other.name;
this.addresses = other.addresses.stream()
.map(Address::new) // deep copy
.toList();
}
}
Customer original = ...;
Customer dup = new Customer(original);
Type-safe, no Cloneable, no CloneNotSupportedException. You control depth explicitly. Recommended for normal classes.
Alternative 2 — Copy factory
A static method with a descriptive name:
public static Customer copyOf(Customer src) {
return new Customer(src); // delegates to copy constructor or constructs fresh
}
This is the JDK idiom for collections: List.copyOf, Set.copyOf, Map.copyOf. Reads better at call sites and gives you room to return a cached instance or a subclass.
Alternative 3 — Manual deep copy (when needed)
For graphs with mutable nested objects, walk the structure yourself:
public Order deepCopy() {
var items = lineItems.stream().map(LineItem::deepCopy).toList();
var addr = new Address(shippingAddress);
return new Order(id, items, addr);
}
Tedious but predictable. Each nested type provides its own deep-copy method.
Alternative 4 — Serialization-based deep copy
For arbitrarily deep object graphs where writing manual copy code would be hours of work:
public static <T extends Serializable> T deepCopy(T obj) throws IOException, ClassNotFoundException {
var baos = new ByteArrayOutputStream();
try (var oos = new ObjectOutputStream(baos)) { oos.writeObject(obj); }
var bais = new ByteArrayInputStream(baos.toByteArray());
try (var ois = new ObjectInputStream(bais)) { return (T) ois.readObject(); }
}
Costs:
- Slow — a serialization round trip per copy.
- Requires every class in the graph to implement
Serializable. - Doesn't deep-copy
transientfields (they revert to defaults).
Use only when:
- The graph is too large to copy by hand.
- You're in an environment that already uses Java serialization (legacy RMI, etc.).
- Tests, throwaway scripts, or other non-hot-path code.
For modern alternatives, Jackson or Kryo can serialize/deserialize for the same effect, often faster and without the Serializable requirement:
ObjectMapper m = new ObjectMapper();
Order copy = m.readValue(m.writeValueAsBytes(order), Order.class);
The easy case — records
If the type is a record and all components are immutable, you don't need a clone at all — share the instance:
public record Point(int x, int y) {}
Point p = new Point(1, 2);
Point sameAsP = p; // no copy needed; Point is immutable
If a component is mutable, build a new record with a fresh copy of the component:
Order o2 = new Order(o.id(), List.copyOf(o.items()), o.address());
Records make Prototype almost vestigial for the simple cases — which is most cases.