概念

Prototype 原型模式

以某个对象作为原型,实现某些方法,能够让外界直接调用该方法来达到复制对象的效果,比如:

  • Java 的 Cloneable 接口;

  • ArraysList 的构造方法可以传入一个集合来复制;

  • Kotlin 中数据类的 copy() 方法;

解决的问题

当需要复制一个对象时, 需要遍历对象中的所有字段,并将字段赋值给新对象的字段;

但是如果存在私有字段,外部无法访问,就会存在问题,如下代码:

class User {
    String name;
    int age;
    private int sex;
    public User(String name, int age, boolean male) {
        this.name = name;
        this.age = age;
        this.sex = male ? 0 : 1;
    }
}

public class Main {
	public static void main(String[] args) {
        User existed = new User("Li-an", 20, true);
        User user = new User(existed.name, existed.age, /* existed.age 无法访问 */);
    }
}

当传入对象的类型是某种抽象类或者接口时,无法得知其确切的类型,也就无法得知其对应的字段;

因此需要额外的类型判断,比如 instanceof 判断后再强转过来处理;

class Person { }

class User extends Person {
    String name;
    int age;
    int sex;

    public User(String name, int age, boolean male) {
        this.name = name;
        this.age = age;
        this.sex = male ? 0 : 1;
    }
}

public class Main {
	public static void main(String[] args) {
        Person existed = new User("Li-an", 20, true);
        if (existed instanceof User) {
            User user = new User(((User)existed).name, ((User)existed).age, ((User)existed).sex == 0);
        }
    }
}

原型模式将克隆过程委派给被克隆的实际对象。(支持多态)支持克隆的对象即为 原型

  • 模式为所有支持克隆的对象声明了一个通用接口 clone 或者 copy() , 该接口能够克隆并返回对象, 同时又无需将代码和对象所属类耦合。

  • 通常情况下, 接口中仅包含一个用于克隆的方法。

为什么不实现一个构造方法,传入已有对象然后对应赋值?这样能够只需要调用构造方法就能实现克隆了。

上面那种多态的情况,传过来的只是接口或者抽象类类型,要调用对应类的构造方法,就需要判断各个类型,当只有少数类时还好,如果类的数量多起来,就需要写很多冗余代码;

实现

定义接口

声明一个接口,内部只有一个抽象方法用于复制对象;

原型对象类只需要实现该接口并实现对应的方法,返回新的对象

interface Copyable {
    
    Copyable copy();
}

class User implements Copyable {
    private String name;
    private int age;
    private int sex;

    public User(String name, int age, boolean male) {
        this.name = name;
        this.age = age;
        this.sex = male ? 0 : 1;
    }

    @Override
    public Copyable copy() {
        return new User(
                name, age, sex == 0
        );
    }
    
}

public class Main {
    public static void main(String[] args) {
        User existed = new User("Li-an", 20, true);
        if (existed instanceof Copyable) {
            User copy = (User) existed.copy();
        }
    }
}

这个和 Java 的 Cloneable 接口很像,但是需要注意:

  • Cloneable 默认实现是 浅拷贝,只是返回的对象与被克隆对象的引用不同(内部字段的引用仍然相等)

  • 当需要实现 深拷贝 时,就需要重写其 cObject#lone() 的克隆方法,同时对于其字段也需要实现 Cloneable 接口来执行克隆操作(递归进行)

构造方法

如果不需要多个类同时实现原型,似乎用接口还复杂了,此时可以重载一个构造方法,负责复制传入的对象:

class User {
    private String name;
    private int age;
    private int sex;
    
    public User(User value) {
        this.name = value.name;
        this.age = value.age;
        this.sex = value.sex;
    }
    

    public User(String name, int age, boolean male) {
        this.name = name;
        this.age = age;
        this.sex = male ? 0 : 1;
    }
}

public class Main {
    public static void main(String[] args) {
        User existed = new User("Li-an", 20, true);
        User copy = new User(existed);
    }
}

Kotlin数据类

Kotlin 的 data class 声明的数据类,编译器会自动生成一个 copy 方法,该方法有多个重载,支持完整复制和修改部分数据的复制;

但是这种方法不适合 Java,因为 Java 的函数参数不能设置默认值

data class User(
	val name: String,
    val age: Int
)

fun main() {
    val user = User("Li", 12)
    val copy = user.copy()
    println(user === copy)
}

深拷贝&浅拷贝

Java 中深拷贝有两种选择:实现 Cloneable 接口 或者 序列化/反序列化

但是无论是哪种方式,深拷贝都会比浅拷贝更耗费资源;

一般来说,原型模式的复制最好使用 深拷贝,毕竟新对象不要影响旧对象;

但是如果深拷贝的性能影响较大时,可以选择深拷贝和浅拷贝结合:

  • 对于不会修改的对象,使用浅拷贝;

  • 对于会修改的对象,使用深拷贝