Java序列化实现对象克隆

前言

  本文是对自己之前一篇博客:浅谈Java对象克隆 的补充,那篇博客对待克隆对象数据域里的对象引用,进行显式的克隆,其它域调用clone方法,也是逐个克隆,最终完成了对象深克隆。本文用序列化的方法进行对象克隆,这种方法是在《Java核心技术 卷2》里第二章看到的。

序列化进行克隆

相关类的定义

// Person类的对象数据域,用来检测是否实现了深克隆
class Pet implements Cloneable ,Serializable{
    String type;
    public Pet(String type) {
        this.type = type;
    }
    public void setType(String type) {
        this.type = type;
    }
    @Override
    public String toString(){
        return this.type;
    }
}
// Person类,用来进行克隆的类
class Person implements Cloneable,Serializable{
    String name;
    int age;
    Pet pet;
    public Person(String name, int age, Pet pet){
        this.name = name;
        this.age = age;
        this.pet = pet;
    }
    public void setPet(Pet pet){
        this.pet = pet;
    }
    public Pet getPet(){
        return pet;
    }
    public void setAge(int age){
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    // 用序列化实现的克隆方法
    public Person clone() throws CloneNotSupportedException {
        // many lines
    }
    @Override
    public String toString(){
        return "name:"+name + " age:" + age + " pet:" + pet;
    }
}
复制代码

  首先看一下克隆相关的类的定义,和 浅谈Java对象克隆 一样,定义了一个Pet类作为Person类的数据域成员,从而通过改变克隆对象的Pet数据域的值检测是否成功进行了深克隆。Pet类和Person类里都有一些set和get方法,以及重写的toString 方法。另外,两个类都实现了Serializable 接口,这同样是个标记接口,想对对象进行序列化,必须实现它。

序列化克隆方法

public Person clone() throws CloneNotSupportedException {
    try {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bout))
        {
            out.writeObject(this);
        }
        try (InputStream bin = new ByteArrayInputStream(bout.toByteArray()))
        {
            ObjectInputStream in = new ObjectInputStream(bin);
            return (Person)in.readObject();
        }
    }
    catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;
}
复制代码

  上一节省略了clone 方法,这一节详细看看,可以看出,先是用ObjectOutputStream流把对象写入了ByteArrayOutputStream流里,进行了序列化,再用ObjectInputStream流从ByteArrayOutputStream流里进行读取,实现反序列化,这样就用序列化和反序列化的思想成功的进行了克隆。因为不涉及对象传输和持久化的问题,只是进行克隆,所以这里并没有序列化到磁盘里。

测试是否为深克隆

    public static void main(String[] args) throws CloneNotSupportedException{
        Person per = new Person("testClone",18,new Pet("cat"));
        Person perClone = per.clone();
        // 设置克隆对象的各个数据域
        perClone.setAge(19);
        perClone.setName("clone");
        perClone.getPet().setType("dog");
        // 输出两个对象
        System.out.println("原对象:"+per);
        System.out.println("克隆对象:"+perClone);
    }
复制代码

  对克隆对象的各个数据域进行修改,用 浅谈Java对象克隆 里同样的方法测试,结果如下:

image.png

  克隆对象的修改没有影响原对象,这种方法同样完成了深克隆。

效率检测

  《Java核心技术 卷2》里提到:这种序列化进行克隆的方法比之前逐个复制数据域的方法会慢很多,接下来测试一下。

    public static void main(String[] args) throws CloneNotSupportedException{
        Person per = new Person("testClone",18,new Pet("cat"));
        long startTime=System.currentTimeMillis();
        for (int i = 0;i < 10000;++i) {
            Person perClone = per.clone();
        }
        long endTime=System.currentTimeMillis();
        System.out.println("运行时间:" + (endTime - startTime) + "ms");
        
    }
复制代码

  测试的主函数如上,进行一万次对象克隆,我们先测试序列化克隆时间:

image.png

  接着把序列化克隆的 clone 方法代码注释掉,clone 方法换成以下传统方法代码:

public Person clone() throws CloneNotSupportedException{
    Person cloned = (Person)super.clone();
    cloned.pet = pet.clone();
    return cloned;
}
复制代码

  运行同样的程序,进行测试,结果如下:

image.png

  可见,效率差了一百倍,猜想可能是输入输出流的序列化写入和读取相较于直接克隆更耗时吧。

  为了验证我的猜想以及满足我自己的好奇心,我把序列化的克隆方法改写如下:

public Person clone() throws CloneNotSupportedException {
        try {
            try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testClone.txt")))
            {
                out.writeObject(this);
            }
            try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("testClone.txt")))
            {
                return (Person)in.readObject();
            }
        }
        catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
复制代码

  现在不是在内存里的ByteArrayOutputStream流里进行写入和读取了,现在把对象序列化到磁盘里的testClone.txt文件里,再反序列化回来,运行同样的时间检测代码,结果如下:

image.png

  可以发现,效率非常感人。同样都是序列化和反序列化,这种方法比借助ByteArrayOutputStream流慢了两百倍,可见,慢就慢在磁盘的读写上了。

总结

  Java的对象序列化和反序列化给我们提供了另外一种对象深克隆的方法。这种方法的优点是编写代码简单,不用把数据域的对象引用单独拎出来重写其clone方法,就可以进行深拷贝;缺点是效率比较感人。