专栏首页Java进阶指南设计模式之原型模式

设计模式之原型模式

我们在创建对象时,通常是通过new关键字来创建的。但是,思考一下,如果当前类的构造函数很复杂,每次new对象时都会消耗非常多的资源,这样肯定是不行的,耗时又费力。

那有没有什么办法解决这种问题呢?当然有,原型模式就可以解决这个痛点。

原型模式非常好理解,就是类的实例对象可以克隆自身,产生新的实例对象,这样就无需用new来创建。想一下,齐天大圣孙悟空是不是拔一根汗毛,就复制出了很多个一模一样的孙悟空,道理是一样的。(新的对象和原对象,内容相同,但是内存地址不同,因为是不同的对象。)

那在Java中,我们怎么实现原型模式呢?非常简单,只需要原型对象实现Cloneable接口就可以了,看代码:(学生对象的复制,学生对象中包含他所学的学科类的对象)

//学科类
public class Subject {
    private String name;
    private String content;

    public String getName() {
        return name;
    }

    public Subject setName(String name) {
        this.name = name;
        return this;
    }

    public String getContent() {
        return content;
    }

    public Subject setContent(String content) {
        this.content = content;
        return this;
    }

    public Subject(String name, String content) {
        this.name = name;
        this.content = content;
    }

    public Subject() {
    }

    @Override
    public String toString() {
        return "Subject{" +
                "name='" + name + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}
//学生类
public class Student implements Cloneable {
    private int age;
    private String name;
    private Subject subject;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public Student(int age, String name, Subject subject) {
        this.age = age;
        this.name = name;
        this.subject = subject;
    }

    public Student() {
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", subject=" + subject +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //实现Cloneable接口,调用父类Object的clone方法来实现对象的拷贝
        return super.clone();
    }
}
public class ProTest {
    public static void main(String[] args) throws Exception {
        Student s1 = new Student(18,"张三",new Subject("语文","这是语文书"));
        //通过调用s1对象的clone方法,即可创建一个新的对象s2
        Student s2 = (Student)s1.clone();

        System.out.println(s1);
        System.out.println(s2);

        s2.setAge(20);
        s2.setName("李四");
        s2.getSubject().setName("数学").setContent("这是数学书");

        System.out.println("=======");
        System.out.println(s1);
        System.out.println(s2);

    }
}

打印结果如下:

Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}}
Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}}
========
Student{age=18, name='张三', subject=Subject{name='数学', content='这是数学书'}}
Student{age=20, name='李四', subject=Subject{name='数学', content='这是数学书'}}

可以发现,s2对象修改的年龄和姓名对原对象s1没有任何影响,但是subject对象修改之后,原对象s1中的subject对象内容也被更改了,这是怎么回事呢?其实,这是因为我们使用的是浅拷贝。(Object对象的clone方法本身就是浅拷贝)

浅拷贝:对值类型的成员变量进行值的复制,对引用类型的成员变量只进行引用的复制,而不复制引用所指向的对象。(嗯?这句话怎么听起来这么熟悉,看下这个:为什么大家都说Java中只有值传递)此时,新对象里边的引用类型变量相当于原对象的引用类型变量的副本,他们指向的是同一个对象。

因此,修改了s2对象的subject对象的内容,原对象s1的subject对象内容也会跟着改变。那如果,我不想让原对象的引用类型变量内容发生改变,应该怎么做呢?

这就要用到深拷贝了,即把引用类型变量的内容也拷贝一份,这样他们就互不影响了。一般有两种方式来实现深拷贝:一种是让需要拷贝的引用类型也实现Cloneable接口,然后重写clone方法;另一种是利用序列化。

clone方式:

还是以上边的例子来说。首先需要修改Subject类让它实现Cloneable接口,重写clone方法。然后修改Student类的clone方法,把所有引用类型的变量手动拷贝一下。

public class Subject implements Cloneable{
    private String name;
    private String content;

    public String getName() {
        return name;
    }

    public Subject setName(String name) {
        this.name = name;
        return this;
    }

    public String getContent() {
        return content;
    }

    public Subject setContent(String content) {
        this.content = content;
        return this;
    }

    public Subject(String name, String content) {
        this.name = name;
        this.content = content;
    }

    public Subject() {
    }

    @Override
    public String toString() {
        return "Subject{" +
                "name='" + name + '\'' +
                ", content='" + content + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Student implements Cloneable {
    private int age;
    private String name;
    private Subject subject;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public Student(int age, String name, Subject subject) {
        this.age = age;
        this.name = name;
        this.subject = subject;
    }

    public Student() {
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", subject=" + subject +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student cloneStu = (Student)super.clone();
        //手动拷贝subject对象,然后赋值给student克隆的新对象
        cloneStu.setSubject((Subject) this.subject.clone());
        return cloneStu;
    }
}

再次运行测试类,会发现修改新对象的引用类型变量已经无法影响原对象的引用类型变量了。

Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}}
Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}}
========
Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}}
Student{age=20, name='李四', subject=Subject{name='数学', content='这是数学书'}}

序列化方式

可以发现,用clone的方式实现起来是非常简单的。但是,考虑如果对象中的引用类型变量很多的时候,这种方式就不太方便 了,因为你要把所有的引用类型都手动clone一遍。另外,如果引用类型又嵌套了多层引用类型,那将是一场灾难。这时,就可以考虑用序列化方式。

系列化方式是通过把对象序列化成二进制流,放到内存中,然后再反序列化成为新的Java对象,这样就可以保证新对象和原对象的互相独立了。

这种方式,需要所有类都实现Serializable接口。Student类只需要添加一个系列化和反序列化的方法就可以了。

public class Subject implements Serializable {
    private String name;
    private String content;

    public String getName() {
        return name;
    }

    public Subject setName(String name) {
        this.name = name;
        return this;
    }

    public String getContent() {
        return content;
    }

    public Subject setContent(String content) {
        this.content = content;
        return this;
    }

    public Subject(String name, String content) {
        this.name = name;
        this.content = content;
    }

    public Subject() {
    }

    @Override
    public String toString() {
        return "Subject{" +
                "name='" + name + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}
public class Student implements Serializable {
    private int age;
    private String name;
    private Subject subject;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public Student(int age, String name, Subject subject) {
        this.age = age;
        this.name = name;
        this.subject = subject;
    }

    public Student() {
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", subject=" + subject +
                '}';
    }


    //深克隆
    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 class ProTest {
    public static void main(String[] args) throws Exception {
        Student s1 = new Student(18,"张三",new Subject("语文","这是语文书"));
        Student s2 = (Student)s1.deepClone();

        System.out.println(s1);
        System.out.println(s2);

        s2.setAge(20);
        s2.setName("李四");
        s2.getSubject().setName("数学").setContent("这是数学书");

        System.out.println("========");
        System.out.println(s1);
        System.out.println(s2);

    }
}

可以看到,在测试类中通过调用deepClone自定义的深克隆方法即可。这种方式适合引用类型或者子嵌套特别多的情况,每个类只需要实现Serializable接口即可,Student的deepClone方法也不需要再改动。

需要注意,这种方式的深拷贝,类中引用类型变量不能用 transient 修饰。(不懂的,自行搜索transient关键字,简单说就是用transient修饰的变量默认不会被序列化)

本文分享自微信公众号 - 烟雨星空(mistyskys),作者:烟雨星空

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-01-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 适配器模式的三种形式

    举个生活中简单的例子,以前的手机内存卡可以取出来,但是想和电脑之间传输音乐、视频等资料不能直接传输,需要通过USB读卡器,然后插入USB接口就可以传输了,这个U...

    烟雨星空
  • Comparable和Comparator兄弟情深?

    Comparable和Comparator乍一看像兄弟俩。但是,虽然长得像,使用上却是有很多区别。

    烟雨星空
  • 常用阻塞队列 BlockingQueue 有哪些?

    之前,介绍了一下 ThreadPoolExecutor 的各参数的含义(并发编程之线程池ThreadPoolExecutor),其中有一个 BlockingQu...

    烟雨星空
  • java8 Lambda尝尝鲜

    转载:猿天地 链接:http://cxytiandi.com/blog/detail/2196 java8都已经发布这么久了,一直没来得及使用,线上环境基本...

    猿天地
  • Java泛型学习

    1、泛型的概念     泛型即“参数化类型”,就比如我们定义方法的时候,定义一个变量,称为形参,变量值根据传进去的实参的值不同而改变。而泛型的出现,就是为了解决...

    JMCui
  • Kotlin —— 这次入门就不用放弃了

    声明:本文是FEELS_CHAOTIC原创,已获其授权发布,未经原作者允许请勿转载

    用户2802329
  • 《从0到1学习Flink》—— 如何自定义 Data Source ?

    在 《从0到1学习Flink》—— Data Source 介绍 文章中,我给大家介绍了 Flink Data Source 以及简短的介绍了一下自定义 Dat...

    zhisheng
  • 手把手教你从零开始做一个好看的 APP

    俗话说,万事开头难,在开始敲代码之前,先让我们来做一些必要的准备,这样才能事半功倍嘛!

    developerHaoz
  • 设计模式之简单工厂

    编写好之后,来看Main方法,当需要苹果时只需要修改一个参数就行了,并不需要在main方法中new,有小伙伴可能要问了,这代码没变少反而多了,关于这个问题请看设...

    tanoak
  • Java基础-day09-代码题-对象;类;封装

    Java基础-day09-代码题-对象&类&封装 一、定义一个空调类和对应的测试类 该题考查点:属性和方法的使用! 要求: 1、空调有品牌和价格两个属性,并且将...

    奋斗蒙

扫码关注云+社区

领取腾讯云代金券