設計模式【5】-- 原型模式

秦怀杂货店發表於2021-12-11

開局一張圖,剩下全靠寫...

設計模式文章集合:http://aphysia.cn/categories/...

111

前言

接觸過 Spring 或者 Springboot 的同學或許都瞭解, Bean 預設是單例的,也就是全域性共用同一個物件,不會因為請求不同,使用不同的物件,這裡我們不會討論單例,前面已經討論過單例模式的好處以及各種實現,有興趣可以瞭解一下:http://aphysia.cn/archives/de...。除了單例以外,Spring還可以設定其他的作用域,也就是scope="prototype",這就是原型模式,每次來一個請求,都會新建立一個物件,這個物件就是按照原型例項建立的。

原型模式的定義

原型模式,也是建立型模式的一種,是指用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件,簡單來說,就是拷貝。一般適用於:

  • 例項比較複雜,完全建立成本高,直接複製比較簡單
  • 建構函式比較複雜,建立可能產生很多不必要的物件

    優點:

  • 隱藏了建立例項的具體細節
  • 建立物件效率比較高
  • 如果一個物件大量相同的屬性,只有少量需要特殊化的時候,可以直接用原型模式拷貝的物件,加以修改,就可以達到目的。

原型模式的實現方式

一般來說,原型模式就是用來複制物件的,那麼複製物件必須有原型類,也就是Prototype,Prototype需要實現Cloneable介面,實現這個介面才能被拷貝,再重寫clone()方法,還可以根據不同的型別來快速獲取原型物件。

我們先定義一個原型類Fruit

public abstract class Fruit implements Cloneable{
    String name;
    float price;

    public String getName() {
        return name;
    }

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

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    public Object clone() {
        Object clone = null;
        try {
            clone = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }

    @Override
    public String toString() {
        return "Fruit{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

以及擴充了Fruit類的實體類Apple,Pear,Watermelon:

public class Apple extends Fruit{
    public Apple(float price){
        name = "蘋果";
        this.price = price;
    }
}
public class Pear extends Fruit{
    public Pear(float price){
        name = "雪梨";
        this.price = price;
    }
}
public class Watermelon extends Fruit{
    public Watermelon(float price){
        name = "西瓜";
        this.price = price;
    }
}

建立一個獲取不同水果類的快取類,每次取的時候,根據不同的型別,取出來,拷貝一次返回即可:

public class FruitCache {
    private static ConcurrentHashMap<String,Fruit> fruitMap =
            new ConcurrentHashMap<String,Fruit>();
    static {
        Apple apple = new Apple(10);
        fruitMap.put(apple.getName(),apple);

        Pear pear = new Pear(8);
        fruitMap.put(pear.getName(),pear);

        Watermelon watermelon = new Watermelon(5);
        fruitMap.put(watermelon.getName(),watermelon);
    }

    public static Fruit getFruit(String name){
        Fruit fruit = fruitMap.get(name);
        return (Fruit)fruit.clone();
    }
}

測試一下,分別獲取不同的水果,以及對比兩次獲取同一種型別,可以發現,兩次獲取的同一種型別,不是同一個物件:

public class Test {
    public static void main(String[] args) {
        Fruit apple = FruitCache.getFruit("蘋果");
        System.out.println(apple);

        Fruit pear = FruitCache.getFruit("雪梨");
        System.out.println(pear);

        Fruit watermelon = FruitCache.getFruit("西瓜");
        System.out.println(watermelon);

        Fruit apple1 = FruitCache.getFruit("蘋果");
        System.out.println("是否為同一個物件" + apple.equals(apple1));
    }
}

結果如下:


Fruit{name='蘋果', price=10.0}
Fruit{name='雪梨', price=8.0}
Fruit{name='西瓜', price=5.0}
false

再測試一下,我們看看裡面的name屬性是不是同一個物件:

public class Test {
    public static void main(String[] args) {
        Fruit apple = FruitCache.getFruit("蘋果");
        System.out.println(apple);

        Fruit apple1 = FruitCache.getFruit("蘋果");
        System.out.println(apple1);
        System.out.println("是否為同一個物件:" + apple.equals(apple1));
        System.out.println("是否為同一個字串物件:" + apple.name.equals(apple1.name));
    }
}

結果如下,裡面的字串確實還是用的是同一個物件:

Fruit{name='蘋果', price=10.0}
Fruit{name='蘋果', price=10.0}
是否為同一個物件:false
是否為同一個字串物件:true

這是為什麼呢?因為上面使用的clone()是淺拷貝!!!不過有一點,字串在Java裡面是不可變的,如果發生修改,也不會修改原來的字串,由於這個屬性的存在,類似於深拷貝。如果屬性是其他自定義物件,那就得注意了,淺拷貝不會真的拷貝該物件,只會拷貝一份引用。

這裡不得不介紹一下淺拷貝與深拷貝的區別:

  • 淺拷貝:沒有真正的拷貝資料,只是拷貝了一個指向資料記憶體地址的指標
  • 深拷貝:不僅新建了指標,還拷貝了一份資料記憶體

如果我們使用Fruit apple = apple1,這樣只是拷貝了物件的引用,其實本質上還是同一個物件,上面的情況雖然物件是不同的,但是Apple屬性的拷貝還屬於同一個引用,地址還是一樣的,它們共享了原來的屬性物件name

那如何進行深拷貝呢?一般有以下方案:

  • 直接 new 物件,這個不用考慮了
  • 序列化與反序列化:先序列化之後,再反序列化回來,就可以得到一個新的物件,注意必須實現Serializable介面。
  • 自己重寫物件的clone()方法

序列化實現深拷貝

序列化實現程式碼如下:

建立一個Student類和School類:

import java.io.Serializable;

public class Student implements Serializable {
    String name;

    School school;

    public Student(String name, School school) {
        this.name = name;
        this.school = school;
    }
}
import java.io.Serializable;

public class School implements Serializable {
    String name;

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

序列化拷貝的類:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class CloneUtil {
    public static <T extends Serializable> T clone(T obj) {
        T result = null;
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(obj);
            objectOutputStream.close();

            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            // 返回生成的新物件
            result = (T) objectInputStream.readObject();
            objectInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}

測試類:


public class Test {
    public static void main(String[] args) {
        School school = new School("東方小學");
        Student student =new Student("小明",school);

        Student student1= CloneUtil.clone(student);
        System.out.println(student.equals(student1));
        System.out.println(student.school.equals(student1.school));
    }
}

上面的結果均是false,說明確實不是同一個物件,發生了深拷貝。

clone實現深拷貝

前面的StudentSchool都實現Cloneable介面,然後重寫clone()方法:


public class Student implements Cloneable {
    String name;

    School school;

    public Student(String name, School school) {
        this.name = name;
        this.school = school;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.school = (School) school.clone();
        return student;
    }
}

public class School implements Cloneable {
    String name;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

測試類:

public class Test {
    public static void main(String[] args) throws Exception{
        School school = new School("東方小學");
        Student student =new Student("小明",school);

        Student student1= (Student) student.clone();
        System.out.println(student.equals(student1));
        System.out.println(student.school.equals(student1.school));
    }
}

測試結果一樣,同樣都是false,也是發生了深拷貝。

總結

原型模式適用於建立物件需要很多步驟或者資源的場景,而不同的物件之間,只有一部分屬性是需要定製化的,其他都是相同的,一般來說,原型模式不會單獨存在,會和其他的模式一起使用。值得注意的是,拷貝分為淺拷貝和深拷貝,淺拷貝如果發生資料修改,不同物件的資料都會被修改,因為他們共享了後設資料。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析JDBCMybatisSpringredis分散式劍指OfferLeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

劍指Offer全部題解PDF

2020年我寫了什麼?

開源程式設計筆記

關注公眾號 ”秦懷雜貨店“ 可以領取劍指 Offer V1版本的 PDF解法,V2版本增加了題目,還在哼哧哼哧的更新中,並且為每道題目增加了C++解法,敬請期待。

相關文章