原型模式的结构与多种实现,包括实现Cloneable接口,并重写clone方法,和复制构造器实现深拷贝,浅拷贝,及使用Serializable实现深拷贝

Posted 阿啄debugIT

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原型模式的结构与多种实现,包括实现Cloneable接口,并重写clone方法,和复制构造器实现深拷贝,浅拷贝,及使用Serializable实现深拷贝相关的知识,希望对你有一定的参考价值。

今天是10月24日,1024程序猿节!

1024程序猿节日由来:因程序员经常使用二进制,2的10次方=1024,所以10月24日也就成为了程序员的节日了......

什么是原型模式?

原型(Prototype)模式的定义:

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个,和原型相同或相似的新对象。

原型模式的结构与实现

由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。

原型模式的结构,包含以下主要角色

  1. 抽象原型类(AbstractPrototype):规定了具体原型对象必须实现的接口。
  2. 具体原型类(concretePrototype):实现抽象原型类的 clone() 方法,它是可被复制的对象
  3. 访问类:使用具体原型类中的 clone() 方法,复制新的对象。 

原型实例指定了要创建的对象的种类。

用这种方式创建对象非常高效,根本无须知道对象创建的细节

原型模式的优点:

  1. Java 自带的原型模式,基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
  2. 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

原型模式的缺点:

  1. 需要为每一个类都配置一个 clone 方法
  2. clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
  3. 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类,都必须支持深克隆,实现起来,会比较麻烦。

因此,深克隆、浅克隆需要运用得当。

原型模式的克隆分为浅克隆和深克隆

深拷贝与浅拷贝有什么区别?直接代码gif对比展示

什么是浅克隆

创建一个新对象,新对象的属性原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址

Subject主题类

@Data
public class Subject {

    private String name;

    public Subject(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

Student实现Cloneable接口,并重写clone方法

@Data
public class Student implements Cloneable {

    //引用类型
    private Subject subject;
    //基础数据类型
    private String name;
    private int age;  

    /**
     *  重写clone()方法
     * @return
     */
    @Override
    public Object clone() {
        //浅拷贝
        try {
            // 直接调用父类的clone()方法
            return super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
   

ShallowCopy 浅拷贝测试 

public class ShallowCopy {
    public static void main(String[] args) {
        Subject subject = new Subject("yuwen");
        Student studentA = new Student();
        studentA.setSubject(subject);
        studentA.setName("Lynn");
        studentA.setAge(20);
        System.out.println("studentA:------------1--" + studentA.toString());
        Student studentB = (Student) studentA.clone();
        studentB.setName("Lily");
        studentB.setAge(18);
        System.out.println("studentB:------------1--" + studentB.toString());
        Subject subjectB = studentB.getSubject();
        subjectB.setName("lishi");
        System.out.println("studentA:------------2--" + studentA.toString());
        System.out.println("studentB:------------2--" + studentB.toString());
    }
}

 可以看出,Student经过拷贝以后,hashCode的值,发生变化;但是内部的属性的hashCode的值,没有变化;只有改变内部属性的值,主类,和子类才发生属性值不同……

浅拷贝并没有做到数据的100%分离,StudentA和StudentB共享同一个Subject对象,所以一个修改会影响到另一个。

浅拷贝总结:

  1. 使用默认的clone方法
  2. 对于原始数据域进行值拷贝
  3. 对于引用类型拷贝引用
  4. 执行快,效率高
  5. 不能做到数据的100%分离。

如果一个对象,只包含原始数据域,或者不可变对象域,推荐使用拷贝。

什么是深克隆

创建一个新对象,属性中引用的其他对象,也会被克隆,不再指向原有对象地址。

Subject主题类

@Data
public class Subject implements Cloneable {

    private String name;
    public Subject(String name) {
        this.name = name;
    }   
    @Override
    protected Object clone() throws CloneNotSupportedException {
        //Subject 如果也有引用类型的成员属性,也应该和 Student 类一样实现
        return super.clone();
    }
    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

Student实现Cloneable接口,并重写clone方法

@Data
public class Student implements Cloneable {
    //引用类型
    private Subject subject;
    //基础数据类型
    private String name;
    private int age;
    /**
     *  重写clone()方法
     * @return
     */
    @Override
    public Object clone() {
        //深拷贝
        try {
            // 直接调用父类的clone()方法
            Student student = (Student) super.clone();
            student.subject = (Subject) subject.clone();
            return student;
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

DeepCopy深拷贝测试 

public class DeepCopy {
    public static void main(String[] args) {
        Subject subject = new Subject("yuwen");
        Student studentA = new Student();
        studentA.setSubject(subject);
        studentA.setName("Lynn");
        studentA.setAge(20);
        System.out.println("studentA:------------1--" + studentA.toString());
        Student studentB = (Student) studentA.clone();
        studentB.setName("Lily");
        studentB.setAge(18);
        System.out.println("studentB:------------1--" + studentB.toString());
        Subject subjectB = studentB.getSubject();
        subjectB.setName("lishi");
        System.out.println("studentA:------------2--" + studentA.toString());
        System.out.println("studentB:------------2--" + studentB.toString());
    }
}

通过打出的日志,可以看出深拷贝的特点:

  • 需要重写clone方法不仅只调用父类的方法,还需调用属性的clone方
  • 做到了原对象与克隆对象之间100%数据分离
  • 如果是对象存在引用类型的属性,建议使用深拷贝
  • 深拷贝比浅拷贝要更加耗时效率更低 

 当我们再次修改StudentB.Subject.name就不会影响到StudentA.Subject.name的值了,因为StudentA和StudentB各自拥有自己的Subject对象,因为做到了数据的100%隔离。

clone使用的场景

某个API需要提供一个List集合,但是又不希望调用者的修改影响到自身的变化,因此需要克隆一份对象,以此达到数据隔离的目的。

日常生产开发,应尽量避免clone的原因

  • 通常情况下,实现接口,是为了表明类可以为它的客户做些什么,而Cloneable仅仅是一个标记接口,而且还改变了super超类中,受保护的方法的行为,是接口的一种极端非典型的用法,不值得效仿。
  • Clone方法,约定及其脆弱,clone方法的Javadoc描述有点暧昧模糊。

“如下为 Java SE8的约定,clone方法创建,并返回该对象的一个拷贝”,

而拷贝的精确含义,取决于该对象的类。

按照惯例,返回的对象应该通过调用super.clone获得。
如果一个类及其所有超类(对象除外)都遵守此约定,则x.clone().getClass() == x.getClass()。

按照约定,此方法返回的对象,应该独立于此对象(正在克隆)。
要实现这种独立性,在super.clone返回的对象之前,可能必须修改该返回对象的一个或多个字段。
通常,这意味着复制,构成被克隆对象内部“深层结构”的任何可变对象,并将对这些对象的引用替换为对副本的引用。
如果类仅包含基本字段或引用不可变对象,则通常情况下super.clone返回的对象中,不需要修改任何字段。

  • 可变对象final域,在克隆方法中,如果我们需要对可变对象的final域也进行拷贝,由于final的限制,所以实际上是无法编译通过的。因此为了实现克隆,我们需要考虑,是否舍去可变对象域的final关键字。

  • 线程安全,如果你决定用线程安全的类,实现Cloneable接口,则需要保证它的clone方法同步工作默认的Object.clone方法是没有做同步的

总的来说,java中的clone方法,实际上并不是完善的,建议尽量避免使用。可以采用“复制构造器”替代clone方案。

复制构造器也可以实现对象的拷贝

复制构造器--实现浅拷贝

public class Car {
 Wheel wheel;
 String manufacturer;
 
 public Car(Wheel wheel, String manufacturer) {
   this.wheel = wheel;
   this.manufacturer = manufacturer;
 }
 
 //浅拷贝copy constructor
 public Car(Car car) {
   this(car.wheel, car.manufacturer);
 }
 
 public static class Wheel {
   String brand;
 }
}

复制构造器--实现深拷贝

public class DeepCar {
    DeepCar.Wheel wheel;
    String manufacturer;

    public DeepCar(DeepCar.Wheel wheel, String manufacturer) {
        this.wheel = wheel;
        this.manufacturer = manufacturer;
    }

    //深拷贝copy constructor
    public DeepCar(DeepCar car) {
        Wheel wheel = new Wheel();
        wheel.brand = car.wheel.brand;

        this.wheel = wheel;
        this.manufacturer = car.manufacturer;
    }

    public static class Wheel {
        String brand;
    }
    //深拷贝
    //使用Serializable实现深拷贝
    public static DeepCar newInstance(DeepCar car) {
        return new DeepCar(car);
    }
}

复制构造器--深拷贝与浅拷贝的区别

 使用Serializable实现深拷贝

运行结果

 由输出结果,可以看出,copy对象的child值修改,不影响example对象的child值,即使用序列化可以实现对象的深拷贝。

深度总结原型模式

虽然面对一个复杂的类层次,且系统必须从其中的许多类型中,创建新对象时,用原型模式相当复杂,但是也是必要选择。毕竟原型模式,可以向客户隐藏制造新实例的复杂性;及提供让客户能够产生未知类型对象的选项;在某些环境下,复制对象比创建新对象,更有效。

当然使用深拷贝,浅拷贝不要只考虑clone方法的使用,还有构造方法,及序列化来更好的实现原型模式的拷贝问题!

在1年1度的1024程序员节日里,祝所有的程序员们“程”风破浪、“码”出未来、横扫BUG。

以上是关于原型模式的结构与多种实现,包括实现Cloneable接口,并重写clone方法,和复制构造器实现深拷贝,浅拷贝,及使用Serializable实现深拷贝的主要内容,如果未能解决你的问题,请参考以下文章

原型模式的结构与多种实现,包括实现Cloneable接口,并重写clone方法,和复制构造器实现深拷贝,浅拷贝,及使用Serializable实现深拷贝

原型模式——HeadFirst设计模式学习笔记

设计模式之原型模式

Java 设计模式之原型学习与掌握

Java 设计模式之原型学习与掌握

GOF之原型模式