【浅谈设计模式】(4) 原型模式--通过复制生成实例

468 阅读5分钟

微信公众号:潇雷

当努力到一定程度,幸运自与你不期而遇。

一、初识原型模式

1.1 概述

今天,开始学习设计模式中创建者模式的第三种--原型模式(prototype)

老规矩,先上百度百科定义:

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

通过一个已经创建好的实例作为原型,然后复制该原型对象得到多个与原型对象一模一样的对象。类似于我们的ctrl+c和ctrl+v组合。简称c-v大法。

下载

那么c-v大法与我们之前的new对象有什么区别呢?

c-v大法出现的原因及它存在的意义是什么呢?

1.2 为什么会出现c-v大法?

为了回答上述的问题,我们就得关注下new关键字和原型模式的区别。原型模式的特点就是克隆。跟new的区别就是,new生成的对象的属性值都是默认的,后续需要我们进行赋值,而克隆生成的对象是将属性值一同复制。

原型模式使用的 clone()底层是直接对二进制数据流操作,最关键的是它不需要调用构造函数,只是内存中的数据块的拷贝。因此,随着jvm性能的提升,和new方式的不断优化之后,new操作的效率并不绝对比clone慢。

下面做个简单的测试:

测试一:

public class User implements Cloneable{
    private String name;
    private Integer age;

    public User() {
    }

    public String getName() {
        return name;
    }

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

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
public class UserTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        user.setName("潇雷");
        user.setAge(18);
        Long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            User user1 = new User();
            user1.setName("leilei");
            user1.setAge(3);
        }
        System.out.println("new 1亿次的时间:"+(System.currentTimeMillis()-beginTime));
        Long beginTime2 = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            User user1 = (User) user.clone();
        }
        System.out.println("clone 1亿次的时间:"+(System.currentTimeMillis()-beginTime2));
    }
}

打印结果:

new 1亿次的时间:382
clone 1亿次的时间:457

在这个例子中,就是一个简单的new对象和赋值对象,因此,在构造1亿个对象的时候,clone会比new对象稍慢一点。但前面我们说了clone不需要在调用构造方法了,如果我们在复杂对象里面加点料,那么二者的对比又是怎样呢?

测试二:

public class User implements Cloneable{
    private String name;
    private Integer age;

    public User() {
        for (int i = 0; i <10; i++) {
            int a=1;
            int b=2;
            int c= (int) (a*b+ Math.PI);
            this.name="xiaolei".concat("xiaoeli").toLowerCase(Locale.ROOT);
        }
    }

    public String getName() {
        return name;
    }

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

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
public class UserTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        user.setName("潇雷");
        user.setAge(18);
        Long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            User user1 = new User();
            user1.setName("leilei");
            user1.setAge(3);
        }
        System.out.println("new 1亿次的时间:"+(System.currentTimeMillis()-beginTime));
        Long beginTime2 = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            User user1 = (User) user.clone();
        }
        System.out.println("clone 1亿次的时间:"+(System.currentTimeMillis()-beginTime2));
    }
}

打印结果:

new 1亿次的时间:20675
clone 1亿次的时间:347

因此,可以惊人的发现,在创建复杂耗时对象的时候,用原型模式达到的效果是非常理想的,能够大大的提升创建对象的效率。

因此,这个例子也回答了为什么会出现c-v大法。

1.3 c-v大法应用场景

这种类型的创建者模式主要用于创建复杂对象,同时又能兼顾到效率。它的使用场景如下:

  • 1、在需要一个类的大量对象的时候,使用原型模式是最佳选择,因为原型模式是内存中对这个对象进行拷贝,如果这个对象的创建非常复杂,用原型模式的效果则越明显。
  • 2、如果一个对象的初始化需要很多对象的数据准备或其他资源的繁琐计算,例如上面我们就在构造方法里进行了初始化计算,这种情况下就可以使用原型模式
  • 3、当需要一个对象的大量公共信息,少量字段进行个性化设置的时候,也可以使用原型模式拷贝出对象进行加工。

二、原型模式实现

2.1 结构

角色如下:

  • 抽象原型类:规定了具体原型对象必须实现的clone方法
  • 具体原型类:实现抽象原型类的clone方法,它是可被赋值的对象
  • 访问类:使用具体原型类中的clone方法来复制新的对象

2.2 克隆它走不走构造方法?

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

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

Java中Object类中提供了 clone() 方法来实现浅克隆。被克隆的对象需要实现Cloneable接口。

public class User implements Cloneable{
    private String name;
    private Integer age;

    public User() {
        System.out.println("采用构造方法生成对象");
    }

    public String getName() {
        return name;
    }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("采用克隆方法生成对象");
        return super.clone();
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
public class UserTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        User user2 = (User) user.clone();
        System.out.println("user 和user2 是否是同一个对象:"+ (user==user2));
    }
}

打印结果:

采用构造方法生成对象
采用克隆方法生成对象
user 和user2 是否是同一个对象:false

在这个例子中,证实了clone确实不走构造方法,而且clone和new生成的对象也不是同一个对象。

2.3 深克隆和浅克隆

上个例子的生成其实就是浅克隆,我们在user类中在加个girlFrient类。看看浅克隆存在什么不足。

public class User implements Cloneable{
    private String name;
    private Integer age;
    private GirlFrient girlFrient;

    public User() {
        System.out.println("采用构造方法生成对象");
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("采用克隆方法生成对象");
        return super.clone();
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    public GirlFrient getGirlFrient() {
        return girlFrient;
    }

    public void setGirlFrient(GirlFrient girlFrient) {
        this.girlFrient = girlFrient;
    }

    @Override
    public String toString() {
        return "User{" +
                "姓名='" + name + '\'' +
                ", 年龄=" + age +
                ", 女朋友名字=" + girlFrient.getName() +
                '}';
    }
}
public class GirlFrient {
    private String name;

    public GirlFrient() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class UserTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        GirlFrient girlFrient = new GirlFrient();
        girlFrient.setName("刘亦菲");
        user.setName("狗子");
        user.setAge(3);
        user.setGirlFrient(girlFrient);

        User user2 = (User) user.clone();
        GirlFrient girlFrient1 = user2.getGirlFrient();
        girlFrient1.setName("乔碧萝");
        user2.setName("猫子");
        user2.setGirlFrient(girlFrient1);
        user2.setAge(18);

        System.out.println("user:"+user.toString());
        System.out.println("user2:"+user2.toString());
    }
}

打印结果:

user:User{姓名='狗子', 年龄=3, 女朋友名字=乔碧萝}
user2:User{姓名='猫子', 年龄=18, 女朋友名字=乔碧萝}

前面也测试过,new出来的对象和clone的对象不是同一个,明明user中狗子的女朋友是刘亦菲 ,但是在经过克隆之后,居然改变了另一个对象的女朋友,这显然是件不合理的事情。

而这就是浅拷贝,java只拷贝你指定的对象,至于你指定的对象里面还有别的对象,它不拷贝,只是把引用给你,共享变量。那其他人都拷贝的话,女朋友类就全指向同一个,显然不符合社会主义价值观!!这是非常不安全的方式,想要女朋友就自己new去。

image-20210606104158634

为了解决这个问题,就出现了深拷贝。如何实现深拷贝?

那就在clone方法中手动实现,弥补这个漏洞。

image-20210606104251719

@Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        GirlFrient girlFrient = new GirlFrient();
        user.setGirlFrient(girlFrient);
        return user;
    }

打印结果:

user:User{姓名='狗子', 年龄=3, 女朋友名字=刘亦菲}
user2:User{姓名='猫子', 年龄=18, 女朋友名字=乔碧萝}

这样就正常了。这就是深拷贝。(猫子痛哭....)

img

三、小结

在本章的几个例子中,主要介绍了原型模式出现的原因和应用场景,原型模式和new对象的区别,并用例子做了说明。原型模式的适用场景,最后介绍了java中浅拷贝和深拷贝之间的区别。这是浅谈设计模式系列的第三种设计模式,感谢你的阅读,你的点赞将是我更新的最好动力。