Prototype Pattern: Efficient Object Cloning in Java
The Prototype pattern enables object creation by copying an existing instance—called a prototype—rather than instantiating classes directly. This approach is especially useful when object initialization is costly or when many similar objects are needed with minor variations.
A basic implementation involves defining a method that creates a new instance and copies the current object's state into it:
public class Item {
private String id;
private String name;
private String description;
public Item cloneManually() {
Item copy = new Item();
copy.id = this.id;
copy.name = this.name;
copy.description = this.description;
return copy;
}
public void display() {
System.out.println("ID: " + id + "; Name: " + name + "; Desc: " + description);
System.out.println("-------------------------------------");
}
// Getters and setters omitted for brevity
}
While this works, Java provides built-in support for cloning through the Cloneable interface and the Object.clone() method. To use it, a class must implement Cloneable and override clone():
public class Item implements Cloneable {
private String id;
private String name;
private String description;
@Override
public Item clone() {
try {
return (Item) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// Other methods remain unchanged
}
This native cloning mechanism avoids constructor invocation and performs a bitwise copy of the object’s memory, offering better performance—especially when creating numerous instances in tight loops.
However, Object.clone() performs a shallow copy by default. For objects containing mutable reference types (e.g., collections), both original and clone will share the same referenced objects:
public class CatalogItem implements Cloneable {
private String title;
private List<String> tags = new ArrayList<>();
public void addTag(String tag) {
tags.add(tag);
}
@Override
public CatalogItem clone() {
try {
CatalogItem copy = (CatalogItem) super.clone();
// Shallow copy: tags list is shared
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
Modifying the tags list in the clone affects the original. To achieve a deep copy, referenced objects must also be cloned:
@Override
public CatalogItem clone() {
try {
CatalogItem copy = (CatalogItem) super.clone();
copy.tags = new ArrayList<>(this.tags); // Deep copy of the list
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
When multiple prototypes exist and may change dynamically, a prototype registry can manage them:
public interface Prototype extends Cloneable {
Prototype duplicate();
}
public class PrototypeRegistry {
private static final Map<String, Prototype> prototypes = new HashMap<>();
private PrototypeRegistry() {}
public static void register(String key, Prototype proto) {
prototypes.put(key, proto);
}
public static void unregister(String key) {
prototypes.remove(key);
}
public static Prototype create(String key) {
Prototype proto = prototypes.get(key);
return proto != null ? proto.duplicate() : null;
}
}
This registry allows clients to request new objects by key without knowing their concrete types.
The Prototype pattern is ideal when:
- Many similar objects are required with slight differences.
- Object construction is expensive or complex.
- Class information is unavailable at runtime, but a sample instence exists.