原型模式

在编码过程中,有些对象创建起来会比较复杂,属性众多,而偏偏是频繁需要的。这时如果按照常规思路来new对象的话,会很麻烦,代码也会冗余。这个时候我们就希望可以利用一个已有的对象来不断对他进行复制就好了,这就是克隆

在原型模式中我们可以利用过一个原型对象来指明我们所要创建对象的类型,然后通过复制这个对象的方法来获得与该对象一模一样的对象实例。这就是原型模式的设计目的。

1、定义

所谓原型模式就是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。

说到克隆,就要区分两个概念:深克隆浅克隆,这里简单解释一下:

  • 浅克隆:只对基本数据类型的属性进行克隆,对非基本数据类型,只复制一份引用。
  • 深克隆:所有属性都复制一份新对象。

    (详情参见深克隆和浅克隆的区别

2、uml图


  原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件: 

  • (1)实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。    
  • (2)重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此Prototype类需要将clone方法的作用域修改为public类型。

3、示例

1)原型类——浅克隆

/*
 * 书本类型,扮演的是ConcretePrototype角色,而Cloneable扮演Prototype角色
 */
public class Book implements Cloneable {

    private String title;// 标题
    private ArrayList<String> image = new ArrayList<String>();// 图片名列表

    public Book() {
        super();
    }

    /**
     * 重写拷贝方法
     * 浅克隆,其他属性没有进行克隆
     */
    @Override
    protected Book clone()  {
        try {
            Book book = (Book) super.clone();
            return book;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    public ArrayList<String> getImage() {
        return image;
    }

    public void addImage(String img) {
        this.image.add(img);
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * 打印内容
     */
    public void showBook() {
        System.out.println("----------------------Start----------------------");

        System.out.println("title:" + title);
        for (String img : image) {
            System.out.println("image name:" + img);
        }

        System.out.println("----------------------End----------------------");
    }
}

2)Client

public class Client {

    public static void main(String[] args) {
        // 1.构建书本对象
        Book book1 = new Book();        
        // 2.编辑书本,添加图片
        book1.setTitle("书1");
        book1.addImage("图1");
        book1.showBook();

        // 以原型文档为原型,拷贝一份副本
        Book book2 = (Book) book1.clone();
        book2.showBook();
        // 修改图书副本,不会影响原始书本
        book2.setTitle("书2");
        book2.addImage("图2");
        book2.showBook();

        // 再次打印原始书本
        book1.showBook();
    }
}

输出:

----------------------Start----------------------
title:书1
image name:图1
----------------------End----------------------
----------------------Start----------------------
title:书1
image name:图1
----------------------End----------------------
----------------------Start----------------------
title:书2
image name:图1
image name:图2
----------------------End----------------------
----------------------Start----------------------
title:书1
image name:图1
image name:图2
----------------------End----------------------

这里用的是浅克隆,所以只有基本数据类型的属性创建了新对象,而image对象是复制了引用,所以book1和book2指向的image是同一个。

3)原型类——深克隆,重写clone方法

/**
     * 重写拷贝方法
     */
    @Override
    protected Book clone()  {
        try {
            Book book = (Book) super.clone();
            // 对image对象也调用clone()函数,进行拷贝
            book.image = (ArrayList<String>) this.image.clone();
            return book;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

输出

----------------------Start----------------------
title:书1
image name:图1
----------------------End----------------------
----------------------Start----------------------
title:书1
image name:图1
----------------------End----------------------
----------------------Start----------------------
title:书2
image name:图1
image name:图2
----------------------End----------------------
----------------------Start----------------------
title:书1
image name:图1
----------------------End----------------------

this.image.clone()实现了对image的克隆,指向的是新的对象。

这里深克隆采用的是添加属性克隆方法的方式,当属性较多时很麻烦,应该采用序列化的方式进行深克隆。

4、特点和使用场景

优点

  • (1)原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,能够简化对象的创建过程,提高效率。
  • (2)还有一个重要的用途就是保护性拷贝,也就是对某个对象对外可能是只读的,为了防止外部对这个只读对象的修改,通常可以通过返回一个对象拷贝的形式实现只读的限制。

缺点

  • (1)这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,在实际开发中应该注意这个潜在问题。优点是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。
  • (2)通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者说成本较高时,通过clone方法才能够获得效率上的提升。
  • (3)对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。

使用场景

  1. 如果创建新对象成本较大,我们可以利用已有的对象进行复制来获得。
  2. 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
  3. 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
  4. Spring框架中bean对象的创建就两种模式:单例模式或者原型模式

注意事项

  1. 被克隆的对象,在克隆的时候构造函数不会执行。Object类的clone方法的原理是从内存中(具体地说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块。

  2. 在开发中,为减少错误,建议在使用原型模式时尽量使用深拷贝,避免操作副本时影响原始对象的问题。如果是在涉及类的继承时,父类有多个引用的情况就非常复杂,建议的方案是深拷贝和浅拷贝分开实现。

  3. 要使用clone方法,类的成员变量上不要增加final关键字。因为final类型是不允许重赋值的。