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

1,001 阅读5分钟

「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战

原型模式(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 对象地址不同,表明这是一个深拷贝。

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

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

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