Java 设计模式——原型模式 (Prototype Pattern)

655 阅读6分钟
什么是原型模式

用于创建重复的对象的最佳方式,同时又能保证性能。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

原型栗子

原型模式实际代码比较简单,但是涉及到【java基本数据类型和引用类型】的概念。下面先看代码


首先我们定义一个Student类,包含各个学科的分数

public class Student implements Cloneable { //实现Cloneable 接口
    float Chinese;
    float Math;
    float English;

    public void setChinese(float chinese) {
        Chinese = chinese;
    }

    public void setMath(float math) {
        Math = math;
    }

    public void setEnglish(float english) {
        English = english;
    }

    @Override
    public String toString() {
        return "Student{" +
                "Chinese=" + Chinese +
                ", Math=" + Math +
                ", English=" + English +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {//重写clone()方法
        return super.clone();//基本数据类型的拷贝
    }
}



通过Student的clone方法,备份一个新的Student类。

        Student student = new Student();
        student.setChinese(110);
        student.setMath(120);
        student.setEnglish(95);
        System.out.println("【原版】" + student.toString());

        Student student_copy = (Student) student.clone();
        System.out.println("【克隆】" + student_copy.toString());

        student_copy.setChinese(150);
        System.out.println("【--原版语文--】" + student.toString());
        System.out.println("【克隆改变语文】" + student_copy.toString());

这里写图片描述
可以看出,备份后的Student_copy改变某个字段是不会影响原版的Student。原型模式大致就是上面这个栗子所描述的,但是有一个比较重要的问题是【clone类的改变不能影响到其它类】

原型的引用类型复制

高二文理分班后,学生的成绩重点不仅仅是看语、数、英,理综/文综也占很大的因素,所以我们举例在学生类中加入内部类- - - -理综类!

    public static class Science {  //Student的内部类
        float Physics;      //物理
        float Chemistry;    //化学
        float Biology;      //生物

        public void setPhysics(float physics) {
            Physics = physics;
        }

        public void setChemistry(float chemistry) {
            Chemistry = chemistry;
        }

        public void setBiology(float biology) {
            Biology = biology;
        }

        @Override
        public String toString() {
            return "Science{" +
                    "Physics=" + Physics +
                    ", Chemistry=" + Chemistry +
                    ", Biology=" + Biology +
                    '}';
        }
    }



老样子,我们备份一个新的Student类。

        Student student = new Student();
        student.setChinese(110);
        student.setMath(120);
        student.setEnglish(95);

        Student.Science science = new Student.Science();
        science.setPhysics(100);
        science.setChemistry(60);
        science.setBiology(40);

        student.setScience(science);//设置理综类

        Student student_copy = (Student) student.clone();//备份一个新的Student类


        System.out.println("【原版】" + student.toString());
        System.out.println("【克隆】" + student_copy.toString());

        student_copy.getScience().setBiology(-1);//因为该考生作弊,所以取消学科分数

        System.out.println("【--原版--】" + student.toString());
        System.out.println("【克隆作弊】" + student_copy.toString());

这里写图片描述
这里写图片描述



我们先把问题解决,再引出这个问题的关键所在。其实导致这个问题的本质原因是我们只进行了浅拷贝,也就是只拷贝了引用,最终两个对象指向的引用是同一个,一个发生变化另一个也会发生变换,显然解决方法就是使用深拷贝。

  1. Science实现Cloneable,并重写clone方法
  2. 修改Studentclone方法,单独克隆Science
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = null;
        try {
            student = (Student) super.clone();
            //引用数据类型:需要单独克隆
            student.science = (Science) this.science.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return student;
    }

这里写图片描述
解析Coloneable

来,让我们撸起袖子看一下这个Coloneable类到底做了什么勾当。
这里写图片描述
看到这个源码让我一度的以为AS出现什么毛病了
这里写图片描述

Cloneable接口之所以没有定义任何的接口的原因那就是在Java中,所有类的终极父类已经将clone()方法定义为所有类都应该具有的基本功能,只是将该方法声明为了protected类型。该方法定义了逐字段拷贝实例的操作。它是一个native本地方法,因此没有实现体,而且在拷贝字段时,除了Object类的字段外,其子类的新字段也将被拷贝到新的实例中。

其实也就是拷贝该类的所有基本数据类型

java基本数据类型和引用类型的存储及区别

堆与栈

这里写图片描述

今天针对上面这个问题我们就分析一下堆和栈【参考rj042的博客】

  • 【栈】存放基本类型的数据和对象的引用
  • 【堆】存放用new产生的数据,即对象本身

Java内存分配中的栈
  在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。

  当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。
  
Java内存分配中的堆
  堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

  在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。 引用变量就相当于是 为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。

基本数据类型和引用类型的区别
基本数据类型

代码是实践真理的唯一准则【参考zejian_的博客】

    int age = 18;
    System.out.println("age==" + age);
    changeAge(age);  //传入age值拷贝
    System.out.println("age==" + age);
    private static void changeAge(int age) {//此处的age是上面传入的age值拷贝
        age = 19;      
    }

这里写图片描述
当传递方法参数类型为基本数据类型(数字以及布尔值)时,一个方法是不可能修改一个基本数据类型的参数。
引用类型
    Student studentW=new Student();
    studentW.setChinese(100);
    System.out.println("语文成绩改变前==" + studentW.getChinese());
    changeChinese(studentW);
    System.out.println("语文成绩作弊后==" + studentW.getChinese());
    private static void changeChinese(Student stu) {
        stu.setChinese(-1);
    }

这里写图片描述

过程分析:

  • stu变量被初始化为studentW值的拷贝,这里是一个对象的引用。
  • 调用stu变量的set方法作用在这个引用对象上,studentW和stu同时引用的Student对象内部值被修改。
  • 方法结束后,stu变量不再使用,被释放,而studentW还是没有变,依然指向Student对象。

当传递方法参数类型为引用数据类型时,一个方法将修改一个引用数据类型的参数所指向对象的值。


总结

所以原型模式的所谓【浅拷贝】就是拷贝了【基本数据类型和引用类型的引用】
【深拷贝】即拷贝【引用类型内的所有基本数据类型的值】
另外List集合存放引用类型的拷贝方法为

        ArrayList<Student> list;
        list.clone();