掘金 后端 ( ) • 2024-04-09 09:51

原型模式

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

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

在不同的面向对象编程语言中,有两种描述对象的方式:

基于类的面向对象:实例是根据类创建的具体对象;类是对象的模板,它定义了对象的属性和方法。如 C++Java... 等语言就是基于类的面向对象。

基于原型的面向对象 :实例是根据原型对象复制出来的;对象可以直接从其他对象克隆而来,而不是通过类的实例化。如JavaScript

基于原型和基于类都能够满足基本的复用和抽象需求,基于原型的方式更加灵活,因为它允许在运行时动态地修改和扩展对象,而不需要预先定义类。相比之下,基于类的方式更加结构化和静态,因为它需要在编译时定义类和实例的结构。

不管怎样,原型模式的核心思想就是通过克隆(复制)现有对象来创建新对象,而不是从头开始实例化新对象。

下面看一下 Java 语言模拟一下对象的复制拟:

基本实现

浅拷贝 Shallow Copy

浅拷贝只会复制对象中的基本数据类型的数据(比如,intlong),以及引用对象(如 数组、对象)的内存地址,不会递归地复制引用对象本身。

JavaObject 类提供了一个 clone()方法,该方法可以将一个 Java 对象浅拷贝一份,但是需要实现一个 Cloneable 接口,该接口表示该类能够复制且具有复制的能力。

@Data
public class ShallowCopy implements Cloneable{
    // 基本类型
    private String name;
    private int age;
    // 引用类型
    public Cat cat;

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


@Data
class Cat{
    private int age;
    private String color;
    private String name;
}


测试如下:

ShallowCopy shallowCopy = new ShallowCopy();
shallowCopy.setName("浅拷贝");
shallowCopy.setAge(18);
shallowCopy.setCat(new Cat());

ShallowCopy cloneShallowCopy = (ShallowCopy)shallowCopy.clone();
System.out.println(shallowCopy);
System.out.println(cloneShallowCopy);
System.out.println(shallowCopy.getCat() ==shallowCopy.getCat() );

输出结果:

ShallowCopy(name=浅拷贝, age=18, cat=Cat(age=0, color=null, name=null))
ShallowCopy(name=浅拷贝, age=18, cat=Cat(age=0, color=null, name=null))
true

可以看到克隆对象 cloneShallowCopy 克隆出来的瞬间就有了原始对象 shallowCopy 的一些数据,不需要再一个个调用 set 方法赋值了。

但要注意的是,浅拷贝对引用类型的复制仅仅是拷贝其引用地址,所以原始对象和克隆对象拥有同一个猫。

深拷贝Deep Copy):

深拷贝不仅仅复制对象的所有基本数据类型的成员变量值,还会为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。

也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝。

@Data
public class DeepCopy implements Cloneable{
    // 基本类型
    private String name;
    private int age;
    // 引用类型
    public Dog dog;

    @Override
    public Object clone() throws CloneNotSupportedException {
        DeepCopy copy = (DeepCopy)super.clone();
        copy.dog = (Dog)copy.dog.clone(); // 深拷贝
        return copy;
    }
}

@Data
class Dog implements Cloneable{
    private int age;
    private String color;
    private String name;
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试如下:

DeepCopy deepCopy = new DeepCopy();
deepCopy.setName("深拷贝");
deepCopy.setAge(20);
deepCopy.setDog(new Dog());
DeepCopy cloneDeepCopy = (DeepCopy)deepCopy.clone();
System.out.println(deepCopy);
System.out.println(cloneDeepCopy);
System.out.println(deepCopy.getDog() ==cloneDeepCopy.getDog() );

输出结果:

DeepCopy(name=深拷贝, age=20, dog=Dog(age=0, color=null, name=null))
DeepCopy(name=深拷贝, age=20, dog=Dog(age=0, color=null, name=null))
false

深拷贝和浅拷贝最大的不同就是对引用类型的处理,引用类型也需要实现 Cloneable 接口,重写 clone() 方法。

根据输出结果可以看出,原始对象和克隆对象拥有不同的狗。

就像在单例模式中讲的序列化与反序列化可以创建出一个新的单例对象一样,深克隆也可以序列化与反序列化实现。

源码鉴赏

JDK 之 集合

全局搜索一下,发现 JDK 中实现 Cloneable 接口的类有很多,其中最主要的就是集合,包括 ListMapSet 都有实现 Cloneable 接口的实现类,以 ArrayList 为例:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

可以看到,ArrayListclone 方法,先是调用父类 Objectclone() 方法创建一个浅拷贝对象(此时原对象中的 elementData 与 克隆对象中的 elementData 指向一个地址);再使用工具类 ArrayscopyOf 方法将原对象中的 elementData 中的元素拷贝到新数组中,新数组由克隆对象管理。

ArrayList 的克隆是浅拷贝还是深拷贝呢?

还是浅拷贝,虽然原对象管理的数组 elementData 与克隆对象中的 elementData 不是同一个数组,但数组中的元素是浅拷贝出来的。

这也就意味着,如果ArrayList存储的是引用类型数据,那克隆出来的ArrayList还是存储这些相同的元素引用。

同样的,JDK 中集合都是浅拷贝。

Spring 之 BeanUtils

BeanUtilsSpring 框架中的一个实用工具类,提供了许多用于处理 Bean 的方法,比如复制属性、获取属性值、设置属性值等操作。

其中 copyProperties()方法就可以用于将一个对象的属性值复制到另一个对象中,代码节选如下:

private static void copyProperties(Object source, Object target) {
    // 遍历目标对象的属性描述符
    for (PropertyDescriptor targetPd : getPropertyDescriptors(target.getClass())) {
        // 获取目标对象属性的写入方法
        Method writeMethod = targetPd.getWriteMethod();
        if (writeMethod != null) {
            // 查找源对象相应属性的属性描述符
            PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
            if (sourcePd != null) {
                // 获取源对象属性的读取方法
                Method readMethod = sourcePd.getReadMethod();
                // 检查源对象属性的读取方法返回类型是否可以赋值给目标对象属性的参数类型
                if (readMethod != null && isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                    try {
                        // 从源对象读取属性值
                        Object value = readMethod.invoke(source);
                        // 将属性值写入目标对象
                        writeMethod.invoke(target, value);
                    } catch (Exception ex) {
                        // 拷贝属性值过程中发生异常
                        throw new RuntimeException("Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                    }
                }
            }
        }
    }
}

可以看到,copyProperties() 方法是使用反射将源对象的属性值复制到目标对象中的,也是浅拷贝。

其实 Spring 还有一个 SerializationUtils 的工具类,可以序列化与反序列化对象,两个方法结合使用也算是实现了深拷贝:

// 创建一个对象 
MyClass original = new MyClass("data"); 
// 将对象序列化为字节数组 
byte[] serialized = SerializationUtils.serialize(original); 
// 将字节数组反序列化为对象 
MyClass deserialized = (MyClass)SerializationUtils.deserialize(serialized);

Spring 的原型作用域也算是原型模式,虽然和单例作用域Bean一样底层也是反射创建的,但 Spring 属性赋值、依赖注入和初始化工作做的好,使得原型Bean创建出来就是完整的。

总结

原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。

优点如下:

比直接new对象性能高:类的初始化会消耗较多的资源,而原型模式的复制仅仅是二进制流的拷贝,性能更高。

简化创建过程:如果使用 new 对象需要非常繁琐的过程,比如构造函数比较复杂,比如初始化需要set的值太多,如果对象的大部分信息在初始化后不发生变化,那就可以考虑原型模式进行代码复用,这既隐藏了对象创建的细节,又对性能是大大的提高。

缺点如下:

必须有克隆方法:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了开闭原则。

必须充分理解深、浅拷贝:对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险,比如如果要拷贝的对象是不可变对象,浅拷贝是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享引用类型的数据,就有可能出现数据被修改的风险。