创建型模式之原型模式

Posted by Night Field's Blog on March 25, 2020

1 概述

原型模式(Prototype Pattern)比较好理解,即以某个对象为原型,创建该对象的副本。我们可以不用知道对象内部的属性以及内部的状态,是迪米特法则的很好体现。

2 原型模式

原型模式一般用在较为复杂对象的创建,并且希望保留对象所持有的状态。Java对这种对象的创建方式也是提供了原生的支持——Object.clone()方法。

1
2
3
public class Object {
    protected native Object clone() throws CloneNotSupportedException;
}

因为Object是所有类的父类,所以Java中所有的类都可以重写clone()方法。当然Object类中的clone()方法也提供了native实现,可以直接通过super.clone()调用,前提是对象需要实现Cloneable接口,否则会报java.lang.CloneNotSupportedException的错误。

3 案例

3.1 浅拷贝

看下面一个例子,用Object类中的clone()方法实现复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Job job = new Job("engineer", "Java coding");
        Person nightfield = new Person(181, 65, job);
        // 复制一个对象
        Person nightfieldCopy = nightfield.clone();

        System.out.format("Height of copied person: %s\n", nightfieldCopy.getHeight());
        System.out.format("Weight of copied person: %s\n", nightfieldCopy.getWeight());
        System.out.format("Job of copied person: %s\n", nightfieldCopy.getJob());

        // 给原对象重新赋值
        nightfield.setHeight(160);
        nightfield.setWeight(80);
        nightfield.getJob().setJobDescription("Python coding");

        System.out.format("Height of copied person: %s\n", nightfieldCopy.getHeight());
        System.out.format("Weight of copied person: %s\n", nightfieldCopy.getWeight());
        System.out.format("Job of copied person: %s\n", nightfieldCopy.getJob());
    }
}

public class Person implements Cloneable {
    private double height;// cm
    private double weight;// kg
    private Job job;

    Person(double height, double weight, Job job) {
        this.height = height;
        this.weight = weight;
        this.job = job;
    }

    public double getHeight() { return height; }

    public void setHeight(double height) { this.height = height; }

    public double getWeight() { return weight; }

    public void setWeight(double weight) { this.weight = weight; }

    public Job getJob() { return job; }

    public void setJob(Job job) { this.job = job; }

    @Override
    protected Person clone() throws CloneNotSupportedException {
        // 直接调用Object的clone()方法
        return (Person)super.clone();
    }
}
public class Job {
    private String jobName;
    private String jobDescription;

    Job(String jobName, String jobDescription) {
        this.jobName = jobName;
        this.jobDescription = jobDescription;
    }

    public void setJobDescription(String description) {this.jobDescription = description }

    public String getJobName() { return jobName; }

    public String getJobDescription() { return jobDescription; }

    @Override
    public String toString() {
        return "Job{" +
                "jobName='" + jobName + '\'' +
                ", jobDescription='" + jobDescription + '\'' +
                '}';
    }
}

输出:

1
2
3
4
5
6
Height of copied person: 181.0
Weight of copied person: 65.0
Job of copied person: Job{jobName='engineer', jobDescription='Java coding'}
Height of copied person: 181.0
Weight of copied person: 65.0
Job of copied person: Job{jobName='engineer', jobDescription='Python coding'}

可以看到,对于基本类型的修改,不会影响副本类;但是对引用对象的修改,会导致副本类也跟着改变。这说明Object类中的clone()方法的默认实现是一个浅拷贝,也就是说副本内部的对象并没有真正复制,而只是复制了引用。 shallow copy

这种机制在很多情况下会导致问题,有没有干净利落的复制方式呢?于是有了深拷贝

3.2 深拷贝

还是刚才的例子,我们通过在clone()方法里手动创建对象并赋值的方式,可以实现深拷贝,下面只给出Person类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Person {
    private double height;// cm
    private double weight;// kg
    private Job job;

    Person(double height, double weight, Job job) {
        this.height = height;
        this.weight = weight;
        this.job = job;
    }

    public double getHeight() { return height; }

    public void setHeight(double height) { this.height = height; }

    public double getWeight() { return weight; }

    public void setWeight(double weight) { this.weight = weight; }

    public Job getJob() { return job; }

    public void setJob(Job job) { this.job = job; }

    @Override
    protected Person clone() {
        Job clonedJob = new Job(job.getJobName(), job.getJobDescription());
        Person person = new Person(height, weight, clonedJob);
        return person;
    }
}

输出:

1
2
3
4
5
6
Height of copied person: 181.0
Weight of copied person: 65.0
Job of copied person: Job{jobName='engineer', jobDescription='Java coding'}
Height of copied person: 181.0
Weight of copied person: 65.0
Job of copied person: Job{jobName='engineer', jobDescription='Java coding'}

对原类的修改,并不影响副本类的值,说明此复制是连同类里面的对象也一起复制了。 deep copy

上面这种深拷贝实现方式,因为需要我们在clone()方法里创建对象并赋值,所以要求我们对类的结构以及属性非常了解。当类比较多或者类的层级很多的时候,会变得很复杂,而且每当该类新增修改属性,都需要修改clone()方法,显然不符合开闭原则

第二种深拷贝的方式,是利用JSON序列化。通过将对象转化成JSON字符串,再转回对象的方式,实现深拷贝,下面只给出关键代码:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
    @Override
    protected Person clone() {
        Gson gson = new GsonBuilder().create();
        // 将对象转成JSON String
        String jsonPerson = gson.toJson(this);
        // 将JSON转化回对象
        Person newPerson = gson.fromJson(jsonPerson, this.getClass());
        return newPerson;
    }
}

通过Gson(或者Jackson)包的帮助,我们可以做到对Person对象的深拷贝。这种方式的好处是很通用,我们只依赖于JSON的类库(一般所有工程都会有)。不过因为它涉及到JSON的转化,所以拷贝效率不是很理想。

第三种深拷贝的方法,是通过Serializable接口提供的序列化与反序列化:先将对象转化成二进制流,然后再转回原对象类型。下面只给出关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Person implements Serializable {
    @Override
    protected DeepPerson serializeClone() {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            // 将对象转成二进制流
            oos.writeObject(this);

            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            // 从二进制中读取对象
            return (DeepPerson) ois.readObject();
        } catch (IOException e) {
            logger.error("Error cloning object", e)
            return null;
        } catch (ClassNotFoundException e) {
            logger.error("Error cloning object", e)
            return null;
        }
    }
}

public class Job implements Serializable {
    ...
}

流序列化的方式,效率比JSON序列化高很多,应该说是最理想的,像ApacheSerializationUtils.clone()就是用的这种方法。不过唯一的限制是,目标对象,以及目标对象引用链下的所有对象,都必须实现Serializable接口(如上例中的Person类和Job类),否则无法成功序列化。

4 总结

原型模式是一种比较简单的创建模式,可以实现对象的复制。应用过程中,应当根据实际情况选择对应的最适合的复制方式。

文中例子的github地址