5. 什么是原型模式 - 图1

1. 原型模式引入

原型模式作为创建型模式的最后一种,它并没有涉及到很多的内容,我们来看一下

首先举一个生活上的例子,例如我们要出版一本书,其中有一些信息字段,例如书名价格等等

  1. public class Book {
  2. private String name; // 姓名
  3. private int price; // 价格
  4. private Partner partner; // 合作伙伴
  5. // 省略构造函数、get set、toString 等
  6. }

引用类型 Partner 也很简单

  1. public class Partner{
  2. private String name;
  3. // 省略构造函数、get set、toString 等
  4. }

1.1 直接 new

书籍出版肯定不能只出一本,如何大批量生产呢?

有人或许想到,像下面这样每一次都重新 new(甚至写个 for 循环),咱先不说大量 new 的效率问题,首先每次一都需要重新给新对象复制,这不就像,我每刊印一本书,就得重新写一次吗???

  1. public class Test {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. // 初始化一个合作伙伴类型
  4. Partner partner = new Partner("张三");
  5. // 带参赋值
  6. Book bookA = new Book("理想二旬不止", 66, partner);
  7. Book bookB = new Book("理想二旬不止", 66, partner);
  8. System.out.println("A: " + bookA.toString());
  9. System.out.println("A: " + bookA.hashCode());
  10. System.out.println("B: " + bookB.toString());
  11. System.out.println("B: " + bookB.hashCode());
  12. }
  13. }

有的同学还或许想到了,先把 A 创建出来,然后再赋值给 B、C ….. 等等,但是这种方式其实是传递引用而不是传值,这就好比在 C 和 B 上写着,内容详情请看 A

  1. public class Test {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. // 初始化一个合作伙伴类型
  4. Partner partner = new Partner("张三");
  5. // 带参赋值
  6. Book bookA = new Book("理想二旬不止", 66, partner);
  7. System.out.println("A: " + bookA.toString());
  8. System.out.println("A: " + bookA.hashCode());
  9. // 引用赋值
  10. Book bookB = bookA;
  11. System.out.println("B: " + bookB.toString());
  12. System.out.println("B: " + bookB.hashCode());
  13. }
  14. }

这两样显然是不行的,我们正常的思路是,作者只需要写一次书籍内容,先刊印一本,如果能行,就照着这个样本进行大批量同彩复印,而上面的传引用方法也显然不合适,这就需要用到Java克隆

1.2 浅克隆

用到克隆,首先就对 Book 类进行处理

  • 首先实现 Cloneable 接口
  • 接着重写 clone 方法

    1. public class Book implements Cloneable{
    2. private String name; // 姓名
    3. private int price; // 价格
    4. private Partner partner; // 合作伙伴
    5. @Override
    6. protected Object clone() throws CloneNotSupportedException {
    7. return super.clone();
    8. }
    9. // 省略构造函数、get set、toString 等
    10. }

    再来测试一下

    1. public class Test {
    2. public static void main(String[] args) throws CloneNotSupportedException {
    3. // 初始化一个合作伙伴类型
    4. Partner partner = new Partner("张三");
    5. // 带参赋值
    6. Book bookA = new Book("理想二旬不止", 66, partner);
    7. // B 克隆 A
    8. Book bookB = (Book) bookA.clone();
    9. System.out.println("A: " + bookA.toString());
    10. System.out.println("A: " + bookA.hashCode());
    11. System.out.println("B: " + bookB.toString());
    12. System.out.println("B: " + bookB.hashCode());
    13. }
    14. }

    执行结果
    A: Book{name=’理想二旬不止’, price=66, partner=Partner{name=张三}}
    A: 460141958
    B: Book{name=’理想二旬不止’, price=66, partner=Partner{name=张三}}
    B: 1163157884

结果非常明显,书籍信息是一致的,但是内存地址是不一样的,也就是说确实克隆成功了,打印其 hashCode 发现两者并不相同,说明不止指向同一个,也是满足我们要求的

到这里并没有结束,你会发现还是有问题,当你刊印的过程中修改一些值的内容的时候,你看看效果

  1. public class Test {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. // 初始化一个合作伙伴类型
  4. Partner partner = new Partner("张三");
  5. // 带参赋值
  6. Book bookA = new Book("理想二旬不止", 66, partner);
  7. // B 克隆 A
  8. Book bookB = (Book) bookA.clone();
  9. // 修改数据
  10. partner.setName("李四");
  11. System.out.println("A: " + bookA.toString());
  12. System.out.println("A: " + bookA.hashCode());
  13. System.out.println("B: " + bookB.toString());
  14. System.out.println("B: " + bookB.hashCode());
  15. }
  16. }

执行结果

A: Book{name=’理想二旬不止’, price=66, partner=Partner{name=李四}}
A: 460141958
B: Book{name=’理想二旬不止’, price=66, partner=Partner{name=李四}}
B: 1163157884

???这不对啊,B 明明是先克隆 A 的,为什么我在克隆后,修改了 A 中一个的值,但是B也变化了啊

这就是典型的浅克隆,在 Book 类,当字段是引用类型,例如 Partner 这个合作伙伴类,就是我们自定义的类,这种情况复制引用不赋值引用的对象,因此,原始对象和复制后的这个Partner对象是引用同一个对象的
5. 什么是原型模式 - 图2

1.3 深克隆

如何解决上面的问题呢,我们需要重新重写 clone 的内容,同时在引用类型中也实现浅克隆

1.3.1 被引用类型实现浅克隆

全代码如下

  1. public class Partner implements Cloneable {
  2. private String name;
  3. @Override
  4. protected Object clone() throws CloneNotSupportedException {
  5. return super.clone();
  6. }
  7. // 省略构造函数、get set、toString 等
  8. }

1.3.2 修改引用类 cloen 方法

  1. public class Book implements Cloneable{
  2. private String name; // 姓名
  3. private int price; // 价格
  4. private Partner partner; // 合作伙伴
  5. @Override
  6. protected Object clone() throws CloneNotSupportedException {
  7. Object clone = super.clone();
  8. Book book = (Book) clone;
  9. book.partner =(Partner) this.partner.clone();
  10. return clone;
  11. }
  12. // 省略构造函数、get set、toString 等
  13. }

测试一下

  1. public class Test {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. // 初始化一个合作伙伴类型
  4. Partner partner = new Partner("张三");
  5. // 带参赋值
  6. Book bookA = new Book("理想二旬不止", 66, partner);
  7. // B 克隆 A
  8. Book bookB = (Book) bookA.clone();
  9. // 修改数据
  10. partner.setName("李四");
  11. System.out.println("A: " + bookA.toString());
  12. System.out.println("A: " + bookA.hashCode());
  13. System.out.println("B: " + bookB.toString());
  14. System.out.println("B: " + bookB.hashCode());
  15. }
  16. }

执行效果

A: Book{name=’理想二旬不止’, price=66, partner=Partner{name=李四}}
A: 460141958
B: Book{name=’理想二旬不止’, price=66, partner=Partner{name=张三}}
B: 1163157884

可以看到,B 克隆 A 后,修改 A 中 合作伙伴 的值,没有受到影响,这也就是我们一开头想要实现的效果了
5. 什么是原型模式 - 图3

2. 原型模式理论

2.1 什么是原型模式

在一些程序中,或许需要创建大量相同或者相似的对象,在构造函数的执行比较缓慢的时候,多次通过传统的构造函数创建对象,就会复杂且耗资源,同时创建时的细节也一样暴露了出来

原型模式就可以帮助我们解决这一问题

定义:原型模式,甩原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象

2.2 结构

5. 什么是原型模式 - 图4

根据结构图简单说一下其中的角色:

  • 抽象原型类(Prototype):规定了具体原型对象必须实现的接口
  • 具体原型类(ConcretePrototype):实现 clone 方法,即一个克隆自身的操作
  • 访问类(Client):使用具体原型类中的 clone 方法克隆自身,从而创建一个新的对象

2.3 两种模式

根据上面的例子也可以看出来了,原型模式分为浅克隆和深克隆

  • 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址
  • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

    2.4 优缺点

    优点:

  • Java 原型模式基于内存二进制流复制,比直接 new 的性能会更好一些

  • 可以利用深克隆保存对象状态,存一份旧的(克隆出来),在对其修改,可以充当一个撤销功能

缺点:

  • 需要配置 clone 方法,改造时需要对已有类进行修改,违背 “开闭原则”
  • 如果对象间存在多重嵌套引用时,每一层都需要实现克隆
    • 例如上例中,Book 中实现深克隆,Partner 中实现浅克隆,所以要注意深浅克隆运用得当