設計模式之原型模式

LXLR發表於2024-09-22

原型模式

一:原型模式概述

為什麼要用原型模式:

在系統中有時候可能需要建立多個一模一樣的物件,而有的物件建立過程十分複雜,或者建立物件很耗費資源亦或是建立物件十分頻繁,那麼這個時候就必須要解決這個問題,而原型模式則能很好的解決這個問題。

基本定義:

原型模式(Prototype Pattern):使用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。原型模式是一種物件建立型模式。

二:原型模式原理結構圖

1545023759228

基本角色

  • Prototype(抽象原型類) 是宣告瞭克隆方法的介面,所有具體原型類的基類,既可以是介面又可以是抽象類,還可以是具體實現類。

  • ConcretePrototype(具體原型類) 實現抽象原型類中的克隆方法,在克隆方法中返回自己一個克隆物件。

三:深克隆與淺克隆

基本概念

  1. 淺複製(淺克隆)

被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。換言之,淺複製僅僅複製所拷貝的物件,而不復制它所引用的物件。

  1. 深複製(深克隆)

    被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深複製把要複製的物件所引用的物件都複製了一遍。

實現java深複製和淺複製的最關鍵的就是要實現Cloneable介面中的clone()方法


如何使用clone()方法

首先我們來看一下Cloneable介面:

官方解釋:

1:實現此介面則可以使用java.lang.Object 的clone()方法,否則會丟擲CloneNotSupportedException 異常

2:實現此介面的類應該使用公共方法覆蓋clone方法

3:此介面並不包含clone 方法,所以實現此介面並不能克隆物件,這只是一個前提,還需覆蓋上面所講的clone方法。

public interface Cloneable {
}
複製程式碼

看看Object裡面的Clone()方法:

  1. clone()方法返回的是Object型別,所以必須強制轉換得到克隆後的型別
  2. clone()方法是一個native方法,而native的效率遠遠高於非native方法,
  3. 可以發現clone方法被一個Protected修飾,所以可以知道必須繼承Object類才能使用,而Object類是所有類的基類,也就是說所有的類都可以使用clone方法
protected native Object clone() throws CloneNotSupportedException;
複製程式碼

小試牛刀:

public class Person {
    public void testClone(){
        super.clone(); // 報錯了
    }
}
複製程式碼

事實卻是clone()方法報錯了,那麼肯定奇怪了,既然Object是一切類的基類,並且clone的方法是Protected的,那應該是可以通過super.clone()方法去呼叫的,然而事實卻是會丟擲CloneNotSupportedException異常, 官方解釋如下:

  1. 物件的類不支援Cloneable介面
  2. 覆蓋方法的子類也可以丟擲此異常表示無法克隆例項。

所以我們更改程式碼如下:

public class Person implements Cloneable{
    public void testClone(){
        try {
            super.clone();
            System.out.println("克隆成功");
        } catch (CloneNotSupportedException e) {
            System.out.println("克隆失敗");
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Person p = new Person();
        p.testClone();
    }
}
複製程式碼

要注意,必須將克隆方法寫在try-catch塊中,因為clone方法會把異常丟擲,當然程式也要求我們try-catch。


java.lang.object規範中對clone方法的約定

  1. 對任何的物件x,都有x.clone() !=x 因為克隆物件與原物件不是同一個物件
  2. 對任何的物件x,都有x.clone().getClass()= =x.getClass()//克隆物件與原物件的型別一樣
  3. 如果物件x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立

對於以上三點要注意,這3項約定並沒有強制執行,所以如果使用者不遵循此約定,那麼將會構造出不正確的克隆物件,所以根據effective java的建議:

謹慎的使用clone方法,或者儘量避免使用。


淺複製例項

  1. 物件中全部是基本型別
public class Teacher implements Cloneable{
    private String name;
    private int age;

    public Teacher(String name, int age){
        this.name = name;
        this.age = age;
    }
    // 覆蓋
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
// 客戶端測試
public class test {
    Teacher origin = new Teacher("tony", 11);
    System.out.println(origin.getName());
    Teacher clone = (Teacher) origin.clone();
    clone.setName("clone");
    System.out.println(origin.getName());
    System.out.println(clone.getName());
}
複製程式碼

結果:

tony tony clone

1545111039834

從執行結果和圖上可以知道,克隆後的值變數會開闢新的記憶體地址,克隆物件修改值不會影響原來物件。

  1. 物件中含有引用型別
public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆蓋
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}

// 學生類
public class Student {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
     public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
// 客戶端測試
public class test {
    public static void main(String[] args) {
        Student student = new Student("學生1" ,11);
        Teacher origin = new Teacher("老師", 11, student);;
        Teacher clone = (Teacher) origin.clone();
        System.out.println("比較克隆後的引用物件");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
        Student student2 = new Student("學生2", 12);
        clone.setStudent(student2);
        System.out.println("克隆後,比較克隆物件改變引用");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
    }
}

複製程式碼

執行結果:

比較克隆後的引用物件 true 克隆後,比較克隆物件改變引用 true

1545109406152

如圖可知,引用型別只會存在一份記憶體地址,執行object的clone方法拷貝的也是引用的複製(這部分的記憶體空間不一樣,)但是引用指向的記憶體空間是一樣的,原物件修改引用變數或者淺拷貝物件修改引用變數都會引起雙方的變化

重點:綜上兩個方面可以知道,Object的clone方法是屬於淺拷貝,基本變數型別會複製相同值,而引用變數型別也是會複製相同的引用。


深複製例項

從上面的淺拷貝可以知道,對於引用的變數只會拷貝引用指向的地址,也就是指向同一個記憶體地址,但是很多情況下我們需要的是下面圖的效果:

1545121868853

深拷貝實現的是對所有可變(沒有被final修飾的引用變數)引用型別的成員變數都開闢記憶體空間所以一般深拷貝對於淺拷貝來說是比較耗費時間和記憶體開銷的。

深拷貝的兩種方式:

  1. 重寫clone方法實現深拷貝

學生類:

public class Student implements Cloneable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

複製程式碼

老師類:

public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆蓋
    @Override
    public Object clone() {
        Teacher t = null;
        try {
            t = (Teacher) super.clone();
            t.student = (Student)student.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return t;
    }
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}
複製程式碼

測試端:

public class test {
    public static void main(String[] args) {
        Student s = new Student("學生1", 11);
        Teacher origin = new Teacher("老師原物件", 23, s);
        System.out.println("克隆前的學生姓名:" + origin.getStudent().getName());
        Teacher clone = (Teacher) origin.clone();
        // 更改克隆後的學生資訊 更改了姓名
        clone.getStudent().setName("我是克隆物件更改後的學生2");
        System.out.println("克隆後的學生姓名:" + clone.getStudent().getName());
    }
}
複製程式碼

執行結果:

克隆前的學生姓名:學生1 克隆後的學生姓名:我是克隆物件更改後的學生2

  1. 序列化實現深克隆

我們發現上面通過object的clone方法去實現深克隆十分麻煩, 因此引出了另外一種方式:序列化實現深克隆

概念:

  • 序列化:把物件寫到流裡
  • 反序列化:把物件從流中讀出來

在Java語言裡深複製一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的一個拷貝)寫到一個流裡,再從流裡讀出來,便可以重建物件。

注意:

  • 寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面
  • 物件以及物件內部所有引用到的物件都是可序列化的
  • 如果不想序列化,則需要使用transient來修飾

案例:

Teacher:

public class Teacher implements Serializable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 深克隆
    public Object deepClone() throws IOException, ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
複製程式碼

Student:

public class Student implements Serializable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
複製程式碼

client:

public class test {
    public static void main(String[] args) {
        try {
            Student s = new Student("學生1", 11);
            Teacher origin = new Teacher("老師原物件", 23, s);
            System.out.println("克隆前的學生姓名:" + origin.getStudent().getName());
            Teacher clone = (Teacher) origin.deepClone();
            // 更改克隆後的d學生資訊 更改了姓名
            clone.getStudent().setName("我是克隆物件更改後的學生2");
            System.out.println("克隆後的學生姓名:" + clone.getStudent().getName());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
複製程式碼

當然這些工作都有現成的輪子了,藉助於Apache Commons可以直接實現:

  • 淺克隆:BeanUtils.cloneBean(Object obj);
  • 深克隆:SerializationUtils.clone(T object);

最後探討

  • 在java中為什麼實現了Cloneable介面,就可以呼叫Object中的Clone方法

    參考以下回答:

www.zhihu.com/question/52…


四:原型模式案例

場景如下:新生入學,每個人都要填寫自己的個人資訊:包括姓名,年齡,身高等等

模板表格:

public class Sheet implements Cloneable {
    private String name;
    private int age;
    private int height;

    // 初始化個人資訊
    public Sheet(String name, int age, int height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    // 提供克隆該例項的方法(淺克隆)
    @Override
    protected Object clone()  {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        return "Sheet{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", height=" + height +
                '}';
    }
}
複製程式碼

客戶端:

public class Client {
    public static void main(String[] args) {
        Sheet sheet = new Sheet("我是模板", 0, 0);
        Sheet sheet1 = (Sheet) sheet.clone();
        sheet1.setName("學生1");
        sheet1.setAge(11);
        sheet1.setHeight(11);
        System.out.println(sheet1.toString());

        Sheet sheet2 = (Sheet) sheet.clone();
        sheet2.setName("學生2");
        sheet2.setAge(11);
        sheet2.setHeight(11);
        System.out.println(sheet2.toString());

    }
}
複製程式碼

執行結果:

Sheet{name='學生1', age=11, height=11} Sheet{name='學生2', age=11, height=11}


四:原型模式總結

  • 優點
    • 如果要建立的物件例項比較複雜,那麼用原型模式可以簡化其建立過程,減少記憶體開銷,提高效率。
    • 可以使用深克隆的方式儲存物件的狀態,通過原型模式將其狀態儲存起來,以便需要時候使用。
  • 缺點
    • 需要為每個類提供克隆方式,極其麻煩,當要擴充套件克隆方法時候,必須修改原始碼,違反開閉原則。
    • 在實現深克隆的時候需要寫大量程式碼,運用起來極其麻煩。

五:使用場景

  1. 建立新物件成本較大(如初始化需要佔用較長的時間,佔用太多的CPU資源或網路資源),新的物件可以通過原型模式對已有物件進行復制來獲得,如果是相似物件,則可以對其成員變數稍作修改。

-- 如果有小夥伴覺得我寫的不錯的話可以關注一下我的部落格,我會一直持續更新,也可以支援一下我的公眾號哦:java架構師小密圈,會分享架構師所必須深入研究的技術,比如netty,分散式,效能優化,spring原始碼分析,mybatis原始碼分析,等等等,同時還會分享一些賺錢理財的小套路哦,歡迎大家來支援,一起學習成長,程式設計師不僅僅是搬瓦工! 公眾號:分享系列好文章

java架構師小密圈

交流群:群友互相分享資料

java架構師小密圈

相關文章