我所理解的设计模式系列·第4篇·原型模式不就是克隆?这么简单我也会!

2021/06/29 设计模式 共 3371 字,约 10 分钟

原型模式(Prototype Patter)属于创建型模式,它提供了一种创建重复对象的最佳方式。其定义是:“用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。”

design-patttern-4-原型模式-封面

1. 模式介绍

原型模式(Prototype Patter)属于创建型模式,它提供了一种创建重复对象的最佳方式。其定义是:”用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。“

2. 应用场景

原型模式主要解决的问题是如何更好地创建一个重复对象

值得注意的是,使用原型模式有一个前提,就是创建对象的成本较大,且新旧对象之间差异并不大,否则就是将简单问题复杂化了。例如对象需要从复杂 IO 中获取时,使用原型模式创建的性能就会比从 IO 中获取的性能要高。

3. 实现方式

在 Java 中,我们可以通过 Object#clone() 实现基于拷贝创建对象,下面是一个简单的示例。

// 用户类
public class User {
    private String name;
    private int age;
    private Address address;

    public User(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
}

// 地址类
public class Address {
    private String addressName;

    public Address(String addressName) {
        this.addressName = addressName;
    }
}

首先创建两个类,User 与 Address,然后在 main 方法中使用 Object#clone() 方法进行克隆操作。

public static void main(String[] args) throws CloneNotSupportedException {
    Address address = new Address("杭州");
    User user = new User("以东", 18, address);
	  System.out.println(user);
    User cloneUser = (User) user.clone();
    System.out.println(cloneUser);
}

运行结果如下:

io.walkers.planes.pandora.design.patterns.prototype.User@6e0be858
Exception in thread "main" java.lang.CloneNotSupportedException: io.walkers.planes.pandora.design.patterns.prototype.User
	at java.lang.Object.clone(Native Method)
	at io.walkers.planes.pandora.design.patterns.prototype.User.main(User.java:31)

咋回事?怎么就报错了呢?不是说在 Java 中通过 Object#clone() 就可以实现克隆吗?想要究其原因,还得翻开 Object#clone() 源码上的注释看看。

Throws:
CloneNotSupportedException  if the object's class does not support the Cloneable interface. Subclasses that override the clone method can also throw this exception to indicate that an instance cannot be cloned.

Object#clone() 方法会抛出 CloneNotSupportedException 异常,在注释中说明了抛出异常的情况:当类没有实现 Cloneable 接口时,调用 Object#clone() 方法会抛出 CloneNotSupportedException 异常。这是 JDK 对于 clone 方法做出的规定,所以当我们在使用 Object#clone() 前需要保证类已经实现了 Cloneable 接口。

在知道原因后,将 User 类实现 Cloneable 接口,再次执行 main 方法,结果如下:

io.walkers.planes.pandora.design.patterns.prototype.User@6e0be858
io.walkers.planes.pandora.design.patterns.prototype.User@61bbe9ba

输出结果中两个 User 对象地址并不相同,表明克隆成功。

但其实这里隐藏了一个细节,要知道在 User 类中包含了一个 Address 对象,目前我们已知在经过 clone 后 User 对象的地址是不同的,那么 Address 对象呢?为了验证这个问题,我们需要重写一下 User 类的 toString 方法。

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

然后再次运行 main 方法,得到输出结果如下:

User{name='以东', age=18, address=io.walkers.planes.pandora.design.patterns.prototype.Address@6e0be858}
User{name='以东', age=18, address=io.walkers.planes.pandora.design.patterns.prototype.Address@6e0be858}

可以看到,Address 对象的地址值是相同的,也就是说在进行克隆时,默认情况下并不会对类中包含的引用类型对象进行克隆,而只是复制一份它的引用地址。

那么如果现在有一个需求是要得到一份完全是新的重复对象,该怎么处理呢?其实这涉及到了原型模式的两种实现方式——浅拷贝与深拷贝。

4. 浅拷贝与深拷贝

在进行数据拷贝时,只拷贝基本类型数据以及引用类型对象的地址,并不会递归地拷贝引用类型对象本身的形式,被称为浅拷贝

相应的,在进行数据拷贝时,除了拷贝基本类型数据,同时会也会将引用类型对象本身(包括引用类型对象中包含的其他引用类型对象)进行拷贝的形式,即拷贝后的对象是完全新的对象,被称为深拷贝

例如在上面介绍的原型模式 demo 示例中,就是浅拷贝的示例(Address 对象的地址相同)。如果我想将它修改成深拷贝形式,则需要重写 User 类的 clone 方法,如下所示:

public class User implements Cloneable {
  	// 省略其他方法与属性
  
		@Override
    protected Object clone() throws CloneNotSupportedException {
        this.address = (Address) address.clone();
        return super.clone();
    }
}

需要注意 Address 类也得实现 Cloneable 接口,如果 Address 类中有其他引用类型的对象,也需要重写 Address 类的 clone 方法,以此递归下去…

在完成改造之后,输出结果如下:

User{name='以东', age=18, address=io.walkers.planes.pandora.design.patterns.prototype.Address@6e0be858}
User{name='以东', age=18, address=io.walkers.planes.pandora.design.patterns.prototype.Address@61bbe9ba}

两个 User 对象中的 Address 对象地址不同,表明这是一个深拷贝。

当然我们还有另一种深拷贝的方式:先将对象序列化,再反序列化为一个新的对象。但是这种方法相对而言更耗时。

关于何时使用浅拷贝,何时使用深拷贝:为了保证修改拷贝后的新对象不会对原对象产生影响,一般建议使用深拷贝。但如果数据量特别特别大,为了避免影响性能,建议使用浅拷贝。

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。

文档信息

Search

    Table of Contents