设计模式之原型模式

我们在创建对象时,通常是通过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修饰的变量默认不会被序列化)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容