Understanding the Prototype Design Pattern in Java

Introduction to the Prototype Pattern

When working with frameworks like Spring, you may be aware that beans are singleton by default, meaning a single instance is shared across all requests. However, Spring also supports other scopes, including scope="prototype", which creates a new instance for each request. This is the essence of the Prototype Pattern.

What is the Prototype Pattern?

The Prototype Pattern is a creational design pattern that specifies the kinds of objects to create using a prototypical instance and creates new objects by copying this prototype. In simpler terms, it's about object cloning. This pattern is particularly useful when:

  • Creating an instance from scratch is expensive or complex
  • The constructor requires complex initialization logic that would create unnecessary overhead

Advantages:

  • Hides the concrete details of object creation
  • Provides better performance when object creation is resource-intensive
  • Allows rapid object generation by cloning and modifying a prototype instead of creating from scratch

Implementing the Prototype Pattern

The standard approach involves creating a prototype class that implements the Cloneable interface and overrides the clone() method. This enables quick object duplication based on the prototype.

Let's create an abstract Vehicle class as our prototype:

public abstract class Vehicle implements Cloneable {
    protected String model;
    protected double basePrice;

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double getBasePrice() {
        return basePrice;
    }

    public void setBasePrice(double basePrice) {
        this.basePrice = basePrice;
    }

    @Override
    protected Object clone() {
        Object cloned = null;
        try {
            cloned = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return cloned;
    }

    @Override
    public String toString() {
        return "Vehicle{" +
                "model='" + model + '\'' +
                ", basePrice=" + basePrice +
                '}';
    }
}

Concrete implementations extending Vehicle:

public class Car extends Vehicle {
    public Car(double price) {
        this.model = "Sedan";
        this.basePrice = price;
    }
}
public class Bike extends Vehicle {
    public Bike(double price) {
        this.model = "Mountain Bike";
        this.basePrice = price;
    }
}
public class Motorcycle extends Vehicle {
    public Motorcycle(double price) {
        this.model = "Sport Bike";
        this.basePrice = price;
    }
}

A cache manager to retrieve cloned vehicle instances:

public class VehicleRegistry {
    private static ConcurrentHashMap<String, Vehicle> vehicleCache =
            new ConcurrentHashMap<>();

    static {
        Car car = new Car(25000);
        vehicleCache.put(car.getModel(), car);

        Bike bike = new Bike(800);
        vehicleCache.put(bike.getModel(), bike);

        Motorcycle motorcycle = new Motorcycle(12000);
        vehicleCache.put(motorcycle.getModel(), motorcycle);
    }

    public static Vehicle getVehicle(String model) {
        Vehicle vehicle = vehicleCache.get(model);
        return (Vehicle) vehicle.clone();
    }
}

Testing the implementation:

public class Main {
    public static void main(String[] args) {
        Vehicle car = VehicleRegistry.getVehicle("Sedan");
        System.out.println(car);

        Vehicle bike = VehicleRegistry.getVehicle("Mountain Bike");
        System.out.println(bike);

        Vehicle motorcycle = VehicleRegistry.getVehicle("Sport Bike");
        System.out.println(motorcycle);

        Vehicle car2 = VehicleRegistry.getVehicle("Sedan");
        System.out.println("Same object: " + car.equals(car2));
    }
}

Output:

Vehicle{model='Sedan', basePrice=25000.0}
Vehicle{model='Mountain Bike', basePrice=800.0}
Vehicle{model='Sport Bike', basePrice=12000.0}
Same object: false

Let's verify the behavior of the model string reference:

public class Main {
    public static void main(String[] args) {
        Vehicle car = VehicleRegistry.getVehicle("Sedan");
        Vehicle car2 = VehicleRegistry.getVehicle("Sedan");

        System.out.println("Different objects: " + !car.equals(car2));
        System.out.println("Same string reference: " + car.model.equals(car2.model));
    }
}

Output:

Different objects: true
Same string reference: true

Understanding Shallow Copy vs Deep Copy

The previous example demonstrates shallow copying. The String field appears identical because strings in Java are immutable—when you modify a string, you actually create a new one rather than modifying the original. This behavior masks potential isssues with mutable reference types.

Key Differences:

  • Shallow Copy: Only copies references to objects, not the actual data. Both original and clone share the same underlying objects.
  • Deep Copy: Creates entirely new objects with copied data. The clone is completely independent of the original.

Using assignment like Vehicle v2 = v1 only copies references—both variables point to the same object. In our shallow copy, while the Vehicle instances differ, their internal reference fields still point to shared objects.

Implementing Deep Copy via Serialization

One reliable approach to deep copying involves serialization and deserialization. The object is serialized to bytes, then deserialized back to create a completely new instance:

import java.io.Serializable;

public class Employee implements Serializable {
    private String name;
    private Department department;

    public Employee(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }
}
import java.io.Serializable;

public class Department implements Serializable {
    private String name;

    public Department(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
import java.io.*;

public class DeepCopyUtil {
    public static <T extends Serializable> T deepClone(T object) {
        T result = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            oos.close();

            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            result = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}
public class Main {
    public static void main(String[] args) {
        Department dept = new Department("Engineering");
        Employee emp = new Employee("Alice", dept);

        Employee empClone = DeepCopyUtil.deepClone(emp);

        System.out.println("Different Employee objects: " + !emp.equals(empClone));
        System.out.println("Different Department objects: " + !emp.getDepartment().equals(empClone.getDepartment()));
    }
}

Both comparisons return false, confirming that deep copying successfully created independent objects.

Implementing Deep Copy via Clone Method

Another approach is to manually override the clone() method to explicitly copy reference fields:

public class Employee implements Cloneable {
    private String name;
    private Department department;

    public Employee(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();
        cloned.department = (Department) this.department.clone();
        return cloned;
    }
}
public class Department implements Cloneable {
    private String name;

    public Department(String name) {
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        Department dept = new Department("Engineering");
        Employee emp = new Employee("Alice", dept);

        Employee empClone = (Employee) emp.clone();

        System.out.println("Different Employee objects: " + !emp.equals(empClone));
        System.out.println("Different Department objects: " + !emp.getDepartment().equals(empClone.getDepartment()));
    }
}

Both assertions confirm deep copying behavior, producing completely independent object graphs.

When to Use the Prototype Pattern

The Prototype Pattern excels in scenarios where object creation is expensive in terms of time or resources, yet many instances share common initial state with only minor variations. It's often combined with other design patterns rather than used in isolation.

Critical consideration: When using cloning, be aware of whether the implementation performs shalow or deep copying. Shallow copies that modify shared mutable state will affect all clones unexpectedly.

Tags: java design-patterns creational-patterns object-cloning cloneable

Posted on Wed, 09 Sep 2026 16:19:08 +0000 by drdapoo