The Prototype Pattern is a creational design pattern that allows you to create new objects by copying existing objects (prototypes) without depending on their classes. This pattern is particularly useful when object creation is expensive or complex.
Key Concepts
- Prototype Interface: Declares the cloning method
- Concrete Prototype: Implements the cloning method
- Client: Creates new objects by copying prototypes
Basic Implementation
Example 1: Basic Prototype Pattern
// Prototype interface
interface Prototype extends Cloneable {
Prototype clone() throws CloneNotSupportedException;
void display();
}
// Concrete prototype
class ConcretePrototype implements Prototype {
private String name;
private int value;
public ConcretePrototype(String name, int value) {
this.name = name;
this.value = value;
System.out.println("Original object created with complex initialization...");
simulateExpensiveOperation();
}
private void simulateExpensiveOperation() {
try {
Thread.sleep(1000); // Simulate expensive operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// Copy constructor for deep cloning
public ConcretePrototype(ConcretePrototype prototype) {
this.name = prototype.name;
this.value = prototype.value;
}
@Override
public Prototype clone() throws CloneNotSupportedException {
// Shallow clone
return (ConcretePrototype) super.clone();
}
public Prototype deepClone() {
// Deep clone using copy constructor
return new ConcretePrototype(this);
}
@Override
public void display() {
System.out.println("Prototype [name=" + name + ", value=" + value + ", hashCode=" + hashCode() + "]");
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getValue() { return value; }
public void setValue(int value) { this.value = value; }
}
// Client code
public class PrototypeBasicDemo {
public static void main(String[] args) throws CloneNotSupportedException {
// Create original object (expensive operation)
ConcretePrototype original = new ConcretePrototype("Original", 100);
original.display();
// Create clone (cheap operation)
ConcretePrototype clone = (ConcretePrototype) original.clone();
clone.setName("Clone");
clone.display();
// Create deep clone
ConcretePrototype deepClone = (ConcretePrototype) original.deepClone();
deepClone.setName("Deep Clone");
deepClone.display();
}
}
Advanced Prototype Pattern with Registry
Example 2: Prototype Registry
import java.util.HashMap;
import java.util.Map;
// Prototype registry
class PrototypeRegistry {
private Map<String, Prototype> prototypes = new HashMap<>();
public void registerPrototype(String key, Prototype prototype) {
prototypes.put(key, prototype);
}
public Prototype getPrototype(String key) throws CloneNotSupportedException {
Prototype prototype = prototypes.get(key);
if (prototype != null) {
return prototype.clone();
}
throw new IllegalArgumentException("Prototype not found for key: " + key);
}
public void loadDefaults() {
registerPrototype("basic", new ConcretePrototype("Basic", 1));
registerPrototype("advanced", new ConcretePrototype("Advanced", 2));
registerPrototype("premium", new ConcretePrototype("Premium", 3));
}
}
// Enhanced prototype with different types
class DocumentPrototype implements Prototype {
private String title;
private String content;
private String author;
private DocumentType type;
private Map<String, String> metadata;
public DocumentPrototype(String title, String content, String author, DocumentType type) {
this.title = title;
this.content = content;
this.author = author;
this.type = type;
this.metadata = new HashMap<>();
simulateDocumentLoading();
}
// Copy constructor
public DocumentPrototype(DocumentPrototype prototype) {
this.title = prototype.title;
this.content = prototype.content;
this.author = prototype.author;
this.type = prototype.type;
this.metadata = new HashMap<>(prototype.metadata); // Deep copy
}
private void simulateDocumentLoading() {
System.out.println("Loading document: " + title);
try {
Thread.sleep(500); // Simulate document loading time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public Prototype clone() throws CloneNotSupportedException {
// Shallow clone - be careful with mutable objects!
return (DocumentPrototype) super.clone();
}
public Prototype deepClone() {
return new DocumentPrototype(this);
}
@Override
public void display() {
System.out.println("Document [title=" + title + ", type=" + type +
", author=" + author + ", contentLength=" + content.length() +
", metadata=" + metadata + "]");
}
// Business methods
public void addMetadata(String key, String value) {
metadata.put(key, value);
}
public void updateContent(String newContent) {
this.content = newContent;
}
// Getters and setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public DocumentType getType() { return type; }
}
enum DocumentType {
REPORT, CONTRACT, PRESENTATION, EMAIL
}
// Usage example
public class PrototypeRegistryDemo {
public static void main(String[] args) throws CloneNotSupportedException {
PrototypeRegistry registry = new PrototypeRegistry();
registry.loadDefaults();
// Create documents from prototypes
DocumentPrototype reportTemplate = new DocumentPrototype(
"Monthly Report", "This is a template for monthly reports.",
"System", DocumentType.REPORT
);
reportTemplate.addMetadata("category", "finance");
reportTemplate.addMetadata("version", "1.0");
registry.registerPrototype("monthly-report", reportTemplate);
// Clone prototypes to create new instances
DocumentPrototype janReport = (DocumentPrototype) registry.getPrototype("monthly-report");
janReport.setTitle("January 2024 Monthly Report");
janReport.setAuthor("John Doe");
janReport.updateContent("January was a great month for sales...");
janReport.addMetadata("period", "January 2024");
janReport.display();
DocumentPrototype febReport = (DocumentPrototype) registry.getPrototype("monthly-report");
febReport.setTitle("February 2024 Monthly Report");
febReport.setAuthor("Jane Smith");
febReport.updateContent("February showed steady growth...");
febReport.addMetadata("period", "February 2024");
febReport.display();
}
}
Real-World Example: Graphic Shapes
Example 3: Graphic Editor with Prototype Pattern
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
// Abstract shape prototype
abstract class Shape implements Cloneable {
protected int x;
protected int y;
protected Color color;
protected boolean selected;
public Shape(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
this.selected = false;
}
// Copy constructor for deep cloning
public Shape(Shape shape) {
this.x = shape.x;
this.y = shape.y;
this.color = shape.color;
this.selected = shape.selected;
}
@Override
public Shape clone() throws CloneNotSupportedException {
return (Shape) super.clone();
}
public abstract Shape deepClone();
public abstract void draw(Graphics g);
public abstract boolean contains(int x, int y);
// Common operations
public void move(int dx, int dy) {
this.x += dx;
this.y += dy;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
// Getters and setters
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
public Color getColor() { return color; }
public void setColor(Color color) { this.color = color; }
}
// Concrete shape: Circle
class Circle extends Shape {
private int radius;
public Circle(int x, int y, int radius, Color color) {
super(x, y, color);
this.radius = radius;
}
// Copy constructor
public Circle(Circle circle) {
super(circle);
this.radius = circle.radius;
}
@Override
public Shape deepClone() {
return new Circle(this);
}
@Override
public void draw(Graphics g) {
g.setColor(color);
g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
if (selected) {
g.setColor(Color.RED);
g.drawOval(x - radius, y - radius, radius * 2, radius * 2);
}
}
@Override
public boolean contains(int pointX, int pointY) {
int dx = pointX - x;
int dy = pointY - y;
return dx * dx + dy * dy <= radius * radius;
}
public int getRadius() { return radius; }
public void setRadius(int radius) { this.radius = radius; }
}
// Concrete shape: Rectangle
class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int x, int y, int width, int height, Color color) {
super(x, y, color);
this.width = width;
this.height = height;
}
// Copy constructor
public Rectangle(Rectangle rectangle) {
super(rectangle);
this.width = rectangle.width;
this.height = rectangle.height;
}
@Override
public Shape deepClone() {
return new Rectangle(this);
}
@Override
public void draw(Graphics g) {
g.setColor(color);
g.fillRect(x, y, width, height);
if (selected) {
g.setColor(Color.RED);
g.drawRect(x, y, width, height);
}
}
@Override
public boolean contains(int pointX, int pointY) {
return pointX >= x && pointX <= x + width &&
pointY >= y && pointY <= y + height;
}
public int getWidth() { return width; }
public void setWidth(int width) { this.width = width; }
public int getHeight() { return height; }
public void setHeight(int height) { this.height = height; }
}
// Graphic editor using prototype pattern
class GraphicEditor {
private List<Shape> shapes = new ArrayList<>();
private Shape selectedShape;
public void addShape(Shape shape) {
shapes.add(shape);
}
public void selectShape(int x, int y) {
// Deselect all shapes
shapes.forEach(shape -> shape.setSelected(false));
selectedShape = null;
// Find and select clicked shape
for (int i = shapes.size() - 1; i >= 0; i--) {
Shape shape = shapes.get(i);
if (shape.contains(x, y)) {
shape.setSelected(true);
selectedShape = shape;
break;
}
}
}
public void duplicateSelectedShape() {
if (selectedShape != null) {
try {
Shape clone = selectedShape.deepClone();
clone.move(20, 20); // Offset the clone
shapes.add(clone);
selectedShape.setSelected(false);
clone.setSelected(true);
selectedShape = clone;
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void drawAll(Graphics g) {
shapes.forEach(shape -> shape.draw(g));
}
public List<Shape> getShapes() { return shapes; }
}
// Demo application
public class GraphicEditorDemo {
public static void main(String[] args) {
// Create prototype shapes
Circle circlePrototype = new Circle(100, 100, 30, Color.BLUE);
Rectangle rectanglePrototype = new Rectangle(200, 100, 80, 60, Color.GREEN);
GraphicEditor editor = new GraphicEditor();
// Add original shapes
editor.addShape(circlePrototype);
editor.addShape(rectanglePrototype);
// Select and duplicate shapes
editor.selectShape(100, 100); // Select circle
editor.duplicateSelectedShape(); // Duplicate circle
editor.selectShape(200, 100); // Select rectangle
editor.duplicateSelectedShape(); // Duplicate rectangle
// Display results
System.out.println("Total shapes: " + editor.getShapes().size());
editor.getShapes().forEach(shape -> {
System.out.println(shape.getClass().getSimpleName() +
" at (" + shape.getX() + ", " + shape.getY() + ")");
});
}
}
Database Entity Prototypes
Example 4: Database Entity Cloning
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
// Base entity prototype
abstract class Entity implements Cloneable {
protected String id;
protected LocalDateTime createdAt;
protected LocalDateTime updatedAt;
protected String createdBy;
public Entity() {
this.id = UUID.randomUUID().toString();
this.createdAt = LocalDateTime.now();
}
// Copy constructor
public Entity(Entity entity) {
this.id = UUID.randomUUID().toString(); // New ID for clone
this.createdAt = LocalDateTime.now();
this.updatedAt = entity.updatedAt;
this.createdBy = entity.createdBy;
}
@Override
public Entity clone() throws CloneNotSupportedException {
return (Entity) super.clone();
}
public abstract Entity deepClone();
// Getters and setters
public String getId() { return id; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
}
// User entity
class User extends Entity {
private String username;
private String email;
private List<String> roles;
private UserProfile profile;
public User(String username, String email) {
super();
this.username = username;
this.email = email;
this.roles = new ArrayList<>();
this.profile = new UserProfile();
}
// Copy constructor for deep cloning
public User(User user) {
super(user);
this.username = user.username;
this.email = user.email;
this.roles = new ArrayList<>(user.roles); // Deep copy
this.profile = user.profile.deepClone(); // Deep copy
}
@Override
public User deepClone() {
return new User(this);
}
public void addRole(String role) {
roles.add(role);
}
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public List<String> getRoles() { return new ArrayList<>(roles); }
public void setRoles(List<String> roles) { this.roles = new ArrayList<>(roles); }
public UserProfile getProfile() { return profile; }
public void setProfile(UserProfile profile) { this.profile = profile; }
@Override
public String toString() {
return "User{id='" + id + "', username='" + username + "', email='" + email +
"', roles=" + roles + ", profile=" + profile + "}";
}
}
// User profile (complex nested object)
class UserProfile implements Cloneable {
private String firstName;
private String lastName;
private String phone;
private Address address;
public UserProfile() {
this.address = new Address();
}
// Copy constructor
public UserProfile(UserProfile profile) {
this.firstName = profile.firstName;
this.lastName = profile.lastName;
this.phone = profile.phone;
this.address = profile.address.deepClone(); // Deep copy
}
public UserProfile deepClone() {
return new UserProfile(this);
}
@Override
public UserProfile clone() throws CloneNotSupportedException {
UserProfile clone = (UserProfile) super.clone();
clone.address = this.address.clone();
return clone;
}
// Getters and setters
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public Address getAddress() { return address; }
public void setAddress(Address address) { this.address = address; }
@Override
public String toString() {
return "UserProfile{firstName='" + firstName + "', lastName='" + lastName +
"', phone='" + phone + "', address=" + address + "}";
}
}
// Address class
class Address implements Cloneable {
private String street;
private String city;
private String zipCode;
public Address deepClone() {
try {
return (Address) this.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Cloning not supported", e);
}
}
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
// Getters and setters
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getZipCode() { return zipCode; }
public void setZipCode(String zipCode) { this.zipCode = zipCode; }
@Override
public String toString() {
return "Address{street='" + street + "', city='" + city + "', zipCode='" + zipCode + "'}";
}
}
// Entity factory using prototype pattern
class EntityFactory {
private User adminUserPrototype;
private User guestUserPrototype;
public EntityFactory() {
// Create prototype instances
adminUserPrototype = new User("admin", "[email protected]");
adminUserPrototype.addRole("ADMIN");
adminUserPrototype.addRole("USER");
adminUserPrototype.getProfile().setFirstName("System");
adminUserPrototype.getProfile().setLastName("Administrator");
guestUserPrototype = new User("guest", "[email protected]");
guestUserPrototype.addRole("GUEST");
}
public User createAdminUser() {
User admin = adminUserPrototype.deepClone();
admin.setCreatedBy("system");
return admin;
}
public User createGuestUser() {
User guest = guestUserPrototype.deepClone();
guest.setCreatedBy("system");
return guest;
}
public User createUserWithProfile(String username, String email, UserProfile profile) {
User user = new User(username, email);
user.setProfile(profile.deepClone());
user.addRole("USER");
return user;
}
}
// Demo
public class EntityPrototypeDemo {
public static void main(String[] args) {
EntityFactory factory = new EntityFactory();
// Create users from prototypes
User admin1 = factory.createAdminUser();
admin1.setUsername("admin1");
admin1.setEmail("[email protected]");
User admin2 = factory.createAdminUser();
admin2.setUsername("admin2");
admin2.setEmail("[email protected]");
User guest = factory.createGuestUser();
guest.setUsername("temporary_guest");
// Create user with custom profile
UserProfile profile = new UserProfile();
profile.setFirstName("John");
profile.setLastName("Doe");
profile.getAddress().setCity("New York");
User customUser = factory.createUserWithProfile("johndoe", "[email protected]", profile);
// Display users
System.out.println("Admin 1: " + admin1);
System.out.println("Admin 2: " + admin2);
System.out.println("Guest: " + guest);
System.out.println("Custom User: " + customUser);
// Verify they have different IDs (different objects)
System.out.println("\nDifferent IDs: " +
(!admin1.getId().equals(admin2.getId()) &&
!guest.getId().equals(customUser.getId())));
}
}
Performance Considerations
Example 5: Prototype vs New Object Creation
public class PerformanceComparison {
static class ExpensiveObject implements Cloneable {
private byte[] data;
public ExpensiveObject() {
// Simulate expensive initialization
this.data = new byte[1024 * 1024]; // 1MB
initializeData();
}
// Copy constructor for prototype
public ExpensiveObject(ExpensiveObject prototype) {
this.data = prototype.data.clone(); // Copy array
}
private void initializeData() {
// Simulate expensive computation
for (int i = 0; i < data.length; i++) {
data[i] = (byte) (i % 256);
}
try {
Thread.sleep(100); // Simulate computation time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public ExpensiveObject clone() throws CloneNotSupportedException {
return (ExpensiveObject) super.clone(); // Shallow clone
}
public ExpensiveObject deepClone() {
return new ExpensiveObject(this);
}
}
public static void main(String[] args) throws CloneNotSupportedException {
int iterations = 100;
// Test creating new objects
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
ExpensiveObject obj = new ExpensiveObject();
}
long newObjectTime = System.currentTimeMillis() - startTime;
// Test cloning from prototype
ExpensiveObject prototype = new ExpensiveObject();
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
ExpensiveObject clone = prototype.deepClone();
}
long cloneTime = System.currentTimeMillis() - startTime;
System.out.println("Time to create " + iterations + " new objects: " + newObjectTime + "ms");
System.out.println("Time to create " + iterations + " clones: " + cloneTime + "ms");
System.out.println("Performance improvement: " +
((double) newObjectTime / cloneTime) + "x faster");
}
}
Best Practices and Considerations
1. Deep vs Shallow Copy
// Shallow copy - be careful with mutable objects!
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
// Deep copy - safer but more expensive
public Prototype deepClone() {
return new ConcretePrototype(this);
}
2. Using Copy Constructors
// Prefer copy constructors over Cloneable
public class MyClass {
public MyClass(MyClass other) {
// Copy all fields, including deep copies of mutable objects
}
}
3. Registry Pattern
// Maintain a registry of prototypes
class PrototypeRegistry {
private Map<String, Prototype> registry = new HashMap<>();
public void register(String key, Prototype prototype) {
registry.put(key, prototype);
}
public Prototype create(String key) {
return registry.get(key).clone();
}
}
When to Use Prototype Pattern
- Object creation is expensive (database calls, network operations)
- System should be independent of how objects are created
- Classes to instantiate are specified at runtime
- Avoid building class hierarchy of factories
- Need to avoid constructor complexity
Advantages and Disadvantages
Advantages:
- Reduces subclassing
- Hides complexity of object creation
- Can add/remove objects at runtime
- Improves performance when object creation is expensive
Disadvantages:
- Complex object copying (deep vs shallow)
- Circular references can be problematic
- Requires careful implementation for mutable objects
The Prototype Pattern is particularly useful in scenarios like graphic editors, game development, and enterprise applications where you need to create multiple similar objects with minor variations.