27 Prototype Pattern and Flyweight Pattern Use These Tools to Elevate System Performance

27 Prototype pattern and Flyweight pattern Use these tools to elevate system performance #

Hello, I’m Liu Chao.

The Prototype Pattern and the Flyweight Pattern are two design patterns that optimize performance in different ways. The former focuses on optimizing the creation process when creating multiple instances, while the latter aims to improve system performance by reducing the number of created instances. At first glance, you might think that these two patterns contradict each other.

However, this is not the case. The usage of these patterns depends on the specific scenario. In some cases, we need to create multiple instances repeatedly, such as assigning an object within a loop. In such situations, we can use the Prototype Pattern to optimize the object creation process. On the other hand, in some scenarios, we can avoid creating multiple instances altogether and simply share objects in memory.

Today, let’s take a look at the applicable scenarios for these two patterns. By understanding them, you will be able to use them more efficiently to improve system performance.

Prototype Pattern #

Let’s first understand the implementation of the Prototype pattern. The Prototype pattern is used to create more objects of the same type by providing a prototype object that specifies the type of the objects to be created. Then, the clone interface implemented by the object itself is used to copy this prototype object. This pattern allows creating objects of the same type without using the new keyword for instantiation. This is because the clone method of the Object class is a native method that can directly manipulate the binary stream in memory, resulting in better performance compared to instantiation using new.

Implementing the Prototype Pattern #

Let’s now implement the Prototype pattern with a simple example:

// Prototype abstract class implementing the Cloneable interface
class Prototype implements Cloneable {
    // Override the clone method
    public Prototype clone(){
        Prototype prototype = null;
        try{
            prototype = (Prototype)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return prototype;
    }
}

// Concrete prototype class implementing the prototype class
class ConcretePrototype extends Prototype{
    public void show(){
        System.out.println("Prototype implementation class");
    }
}

public class Client {
    public static void main(String[] args){
        ConcretePrototype cp = new ConcretePrototype();
        for(int i=0; i< 10; i++){
            ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
            clonecp.show();
        }
    }
}

To implement a prototype class, three conditions need to be met:

  • Implement the Cloneable interface: The Cloneable interface is similar to the Serializable interface. It simply tells the virtual machine that the clone method can be safely used on classes implementing this interface. Only classes that implement the Cloneable interface can be copied in the JVM, otherwise a CloneNotSupportedException exception will be thrown.
  • Override the clone method in the Object class: In Java, the parent class of all classes is the Object class, which has a clone method that returns a copy of the object.
  • Call super.clone() in the overridden clone method: By default, a class does not have the ability to copy an object, so super.clone() needs to be called to implement it.

From the above, we can see that the main feature of the Prototype pattern is to copy an object using the clone method. Usually, some people mistakenly believe that Object a=new Object(); Object b=a; is a process of object copying. However, this kind of copying is only for object references, which means that both a and b objects point to the same memory address. If b is modified, the value of a will also be modified.

We can check the issue of ordinary object copying through a simple example:

class Student {  
    private String name;  
    
    public String getName() {  
        return name;  
    }  
    
    public void setName(String name) {  
        this.name = name;  
    }  
}  

public class Test {  
    public static void main(String args[]) {  
        Student stu1 = new Student();  
        stu1.setName("test1");  

        Student stu2 = stu1;  
        stu1.setName("test2");  

        System.out.println("Student 1:" + stu1.getName());  
        System.out.println("Student 2:" + stu2.getName());  
    }  
}

If it is an object copy, the log printed at this time should be:

Student 1:test1
Student 2:test1

However, the actual output is:

Student 1:test2
Student 2:test2

Only the objects copied using the clone method are truly cloned, and the objects assigned by the clone method are completely independent objects. As mentioned earlier, the clone method in the Object class is a native method that directly manipulates the binary stream in memory. This is especially obvious when copying large objects. Let’s implement the above example again using the clone method:

// Student class implementing the Cloneable interface
class Student implements Cloneable {  
    private String name;  // Name
    
    public String getName() {  
        return name;  
    }  
    
    public void setName(String name) {  
        this.name = name;  
    }  
    
    // Override the clone method
    public Student clone() { 
        Student student = null; 
        try { 
            student = (Student) super.clone(); 
        } catch (CloneNotSupportedException e) { 
            e.printStackTrace(); 
        } 
        return student; 
    }  
}  

public class Test {  
    public static void main(String args[]) {  
        Student stu1 = new Student();  // Create student 1
        stu1.setName("test1");  

        Student stu2 = stu1.clone();  // Create student 2 through cloning
        stu2.setName("test2");  

        System.out.println("Student 1:" + stu1.getName());  
        System.out.println("Student 2:" + stu2.getName());  
    }  
}

Output: Student 1: test1 Student 2: test2

Deep Copy and Shallow Copy #

After calling the super.clone() method, the current object’s class is checked to see if it supports cloning, which means checking whether the class has implemented the Cloneable interface.

If it is supported, a new object of the current object’s class is created and initialized so that the values of its member variables are exactly the same as the values of the current object’s member variables. However, for references to other objects and member attributes of types such as List, only the references to these objects can be copied. Therefore, simply calling super.clone() is a shallow copy.

So, when we use the clone() method to clone an object, we need to be aware of the problems caused by shallow copy. Let’s look at an example to see shallow copy in action.

// Define the Student class
class Student implements Cloneable{
    private String name; // Student name
    private Teacher teacher; // Define the Teacher class

    public String getName() {
        return name;
    }

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

    public Teacher getTeacher() {
        return teacher;
    }

    public void setName(Teacher teacher) {
        this.teacher = teacher;
    }

    // Override the clone method
    public Student clone() {
        Student student = null;
        try {
            student = (Student) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return student;
    }

}

// Define the Teacher class
class Teacher implements Cloneable{
    private String name; // Teacher name

    public String getName() {
        return name;
    }

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

    // Override the clone method to clone the Teacher class
    public Teacher clone() {
        Teacher teacher = null;
        try {
            teacher= (Teacher) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return teacher;
    }

}
public class Test {

    public static void main(String args[]) {
        Teacher teacher = new Teacher (); // Define Teacher 1
        teacher.setName(" 刘老师 ");
        Student stu1 = new Student(); // Define Student 1
        stu1.setName("test1");
        stu1.setTeacher(teacher);

        Student stu2 = stu1.clone(); // Define Student 2
        stu2.setName("test2");
        stu2.getTeacher().setName(" 王老师 ");// Modify the teacher
        System.out.println(" Student " + stu1.getName() + " has teacher: " + stu1.getTeacher().getName());
        System.out.println(" Student " + stu1.getName() + " has teacher: " + stu2.getTeacher().getName());
    }
}

Output:

Student test1 has teacher: 王老师
Student test2 has teacher: 王老师

From the above output, we can see that when we modify the teacher for Student 2, the teacher for Student 1 is also modified. This is the problem caused by shallow copy.

We can solve this problem by using deep copy, which is essentially based on shallow copy but recursively clones each object. The code is as follows:

public Student clone() { Student student = null; try { student = (Student) super.clone(); Teacher teacher = this.teacher.clone(); // Clone the teacher object student.setTeacher(teacher); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return student; }

Applicable Scenarios #

I have explained in detail the implementation principle of the prototype pattern, but when should we use it?

We can use the prototype pattern to improve the performance of object creation in scenarios where objects are repeatedly created. For example, as mentioned earlier, when creating objects inside a loop, we can consider using the clone method to achieve this.

For example:

for(int i=0; i<list.size(); i++){
  Student stu = new Student();
  ...
}

We can optimize it to:

Student stu = new Student();
for(int i=0; i<list.size(); i++){
 Student stu1 = (Student)stu.clone();
  ...
}

In addition, the prototype pattern is also widely used in open-source frameworks. For example, in Spring, @Service is by default a singleton. If a private global variable is used, and we don’t want to affect the next injection or each context’s bean retrieval, we need to use the prototype pattern. We can achieve this using the @Scope("prototype") annotation.

Flyweight Pattern #

The Flyweight pattern is a design pattern that effectively reuses fine-grained objects using shared techniques. In this pattern, objects can be divided into internal data and external data based on their information states. Internal data are information that objects can share, and these information do not change during the system’s runtime. External data, on the other hand, are marked with different values during different runtimes.

The Flyweight pattern generally consists of three roles: Flyweight (abstract flyweight class), ConcreteFlyweight (concrete flyweight class), and FlyweightFactory (flyweight factory class). The abstract flyweight class is usually an interface or an abstract class that provides the internal and external data of the flyweight objects to the outside world. The concrete flyweight class refers to the class that implements the sharing of internal data. The flyweight factory class is mainly used to create and manage flyweight objects.

Implementing the Flyweight Pattern #

Let’s implement the Flyweight pattern through a simple example:

// Abstract flyweight class
interface Flyweight {
    // External state object
    void operation(String name);
    // Internal object
    String getType();
}

// Concrete flyweight class
class ConcreteFlyweight implements Flyweight {
    private String type;
 
    public ConcreteFlyweight(String type) {
        this.type = type;
    }
 
    @Override
    public void operation(String name) {
        System.out.printf("[Type (Internal State)] - [%s] - [Name (External State)] - [%s]\n", type, name);
    }
 
    @Override
    public String getType() {
        return type;
    }
}

// Flyweight factory class
class FlyweightFactory {
    private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>(); // Flyweight pool for storing flyweight objects

    public static Flyweight getFlyweight(String type) {
        if (FLYWEIGHT_MAP.containsKey(type)) { // If the object already exists in the flyweight pool, return it directly
            return FLYWEIGHT_MAP.get(type);
        } else { // If it does not exist in the pool, create a new object and put it into the flyweight pool
            ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
            FLYWEIGHT_MAP.put(type, flyweight);
            return flyweight;
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Flyweight fw0 = FlyweightFactory.getFlyweight("a");
        Flyweight fw1 = FlyweightFactory.getFlyweight("b");
        Flyweight fw2 = FlyweightFactory.getFlyweight("a");
        Flyweight fw3 = FlyweightFactory.getFlyweight("b");
        fw1.operation("abc");
        System.out.printf("[Result (Object Comparison)] - [%s]\n", fw0 == fw2);
        System.out.printf("[Result (Internal State)] - [%s]\n", fw1.getType());
    }
}

Output:

[Type (Internal State)] - [b] - [Name (External State)] - [abc]
[Result (Object Comparison)] - [true]
[Result (Internal State)] - [b]

By observing the running result of the above code, we can see that if the object already exists in the flyweight pool, it will not be created again; instead, it will reuse the object with consistent internal data in the flyweight pool. This reduces the creation of objects and saves memory space occupied by objects with the same internal data.

Applicable Scenarios #

The Flyweight pattern is widely used in practical development. For example, in Java, the String class can share string objects in the constant pool, which reduces the creation of duplicate objects and saves memory space. The code is as follows:

String s1 = "hello";
String s2 = "hello";
System.out.println(s1==s2); // true

In addition, the pattern also has applications in daily development. For example, a thread pool is an implementation of the Flyweight pattern. By storing products in the application service’s cache, there is no need to retrieve the product information from Redis cache or database every time a user retrieves the product information, thus avoiding the repeated creation of product information in memory.

Summary #

Through the above explanation, I believe you now have a clearer understanding of the Prototype pattern and the Flyweight pattern. Both patterns are widely used in open source frameworks and actual development.

When we need to create a large number of identical objects, we can use the Prototype pattern to copy objects through the clone method. This approach is more efficient than using new and serialization to create objects. When creating objects, if we can share the internal data of the objects, we can reduce object creation and achieve system optimization by sharing objects with the same internal data using the Flyweight pattern.

Thought Question #

The singleton pattern discussed in the previous lesson and the flyweight pattern discussed in this lesson are both designed to avoid duplicate object creation. Do you know the difference between the two?