设计模式之原型模式

原型模式

一:原型模式概述

为什么要用原型模式:

在系统中有时候可能需要创建多个一模一样的对象,而有的对象创建过程十分复杂,或者创建对象很耗费资源亦或是创建对象十分频繁,那么这个时候就必须要解决这个问题,而原型模式则能很好的解决这个问题。

基本定义:

原型模式(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,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,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("老师原对象",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方法

    参考以下回答:

https://www.zhihu.com/question/52490586

四:原型模式案例

场景如下:新生入学,每个人都要填写自己的个人信息:包括姓名,年龄,身高等等

模板表格:

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

    // 初始化个人信息
    public Sheet(String name,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);
        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',height=11}

四:原型模式总结

  • 优点
    • 如果要创建的对象实例比较复杂,那么用原型模式可以简化其创建过程,减少内存开销,提高效率。
    • 可以使用深克隆的方式保存对象的状态,通过原型模式将其状态保存起来,以便需要时候使用。
  • 缺点
    • 需要为每个类提供克隆方式,极其麻烦,当要扩展克隆方法时候,必须修改源代码,违反开闭原则。
    • 在实现深克隆的时候需要写大量代码,运用起来极其麻烦。

五:使用场景

  1. 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结