4

为什么要克隆?

我在平时的Java开发中很少用到克隆,最近上课遇到老师提出一个问题,如何复制一个已经存在的对象? 当然我们可以new一个对象,然后对它的属性进行赋值,但这样太过麻烦,而且执行速度也会变慢。我们现在就可以使用Clone的方法。

现在假设我们有一个Animal类,一个Zoo类,Animal是Zoo的成员对象,代码如下

public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
public class Zoo {
    private int number;
    private String name;
    private Animal[] animals;

    public Zoo() {

    }

    public Zoo(int number, String name) {
        this.number = number;
        this.name = name;
    }

    public Zoo(int number, String name, Animal[] animals) {
        this.number = number;
        this.name = name;
        this.animals = animals;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Animal[] getAnimals() {
        return animals;
    }

    public void setAnimals(Animal[] animals) {
        this.animals = animals;
    }

    @Override
    public String toString() {
        String description = number + " " + name;
        if (this.animals != null) {
            for (int i = 0; i < this.animals.length; i++) {
                description += " " + this.animals[i].getName();
            }
        }

        return description;
    }
}

这是我们的测试类

public class CloneTest {
    public static void main(String[] args) {

        Zoo zoo1 = new Zoo(1, "动物园1");
        Animal[] animals1 = new Animal[] { new Animal("老虎"), new Animal("狮子") };
        zoo1.setAnimals(animals1);

        Zoo zoo2 = new Zoo();

        System.out.println(zoo1);
        System.out.println(zoo2);
        System.out.println("----------------------");

        zoo2 = zoo1;

        System.out.println(zoo1);
        System.out.println(zoo2);
        System.out.println("----------------------");

        zoo1.setName("动物园10");// 动物园1改名了
        zoo1.setNumber(10);
        System.out.println(zoo1);
        System.out.println(zoo2);
    }
}

我们new了一个zoo1,并对它的属性赋值,这时我们想要克隆一个和zoo1一模一样的对象zoo2,可能像我一样的小白会这样做

zoo2 = zoo1;

我们可以看下控制台的输出结果
QQ截图20200506082514.png

我们发现只要zoo1的属性值发生改变,zoo2也会随之改变,如果你打印两个对象的地址,你会发现是相同的。这是因为上面那行代码只是简单地将zoo2指向了zoo1在堆中引用的对象。

20200505182216.png20200505182256.png

显然这并不能达到我们复制一个对象的目的。由此,我们引出克隆(Clone)。

我们先了解下浅克隆和深克隆的定义:
浅克隆:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
深克隆:除去那些引用其他对象的变量,被复制对象的所有变量都含有与原来的对象相同的值。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

如何实现克隆

下面是浅克隆的实现步骤,深克隆需要重写Clone()

  1. 对象的类实现Cloneable接口;
  2. 覆盖Object类的clone()方法(覆盖clone()方法,访问修饰符设为public,默认是protected,但是如果所有类都在同一个包下protected是可以访问的);
  3. 在clone()方法中调用super.clone();

浅克隆

首先我们实现下浅克隆。我们让Zoo实现Cloneable接口

public class Zoo implements Cloneable {
    private int number;
    private String name;
    private Animal[] animals;

    public Zoo() {

    }

    public Zoo(int number, String name) {
        this.number = number;
        this.name = name;
    }

    public Zoo(int number, String name, Animal[] animals) {
        this.number = number;
        this.name = name;
        this.animals = animals;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Animal[] getAnimals() {
        return animals;
    }

    public void setAnimals(Animal[] animals) {
        this.animals = animals;
    }

    @Override
    public String toString() {
        String description = number + " " + name;
        if (this.animals != null) {
            for (int i = 0; i < this.animals.length; i++) {
                description += " " + this.animals[i].getName();
            }
        }

        return description;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        /*
         * Zoo zoo2 = (Zoo) super.clone(); if (this.animals != null &&
         * this.animals.length > 0) { Animal[] animal2 = new
         * Animal[this.animals.length]; for (int i = 0; i < this.animals.length; i++) {
         * animal2[i] = (Animal) this.animals[i].clone(); } zoo2.setAnimals(animal2); }
         * 
         * return zoo2;
         */
        return super.clone();
    }
    }

重新修改下我们的测试类

package cn.wuzheyi.clone1;

public class CloneTest1 {
    public static void main(String[] args) throws CloneNotSupportedException {

        Zoo zoo1 = new Zoo(1, "动物园1");
        Animal[] animals1 = new Animal[] { new Animal("老虎"), new Animal("狮子") };
        zoo1.setAnimals(animals1);

        Zoo zoo2 = new Zoo();

        System.out.println(zoo1);
        System.out.println(zoo2);
        System.out.println("----------------------");

        zoo2 = (Zoo) zoo1.clone();

        System.out.println(zoo1);
        System.out.println(zoo2);
        System.out.println("----------------------");

        zoo1.setName("动物园10");// 动物园1改名了
        zoo1.setNumber(10);
        System.out.println(zoo1);
        System.out.println(zoo2);
        System.out.println("----------------------");

        zoo1.getAnimals()[0] = new Animal("熊猫"); // 原动物园1将老虎换为了熊猫
        System.out.println(zoo1);
        System.out.println(zoo2);
    }
}

执行我们的测试类,控制台输入如下
QQ截图20200506091559.png

可以看出我们的基本数据类型会复制相同值,而引用变量类型也是会复制相同的引用。所以我们在修改zoo1中的成员对象时,zoo2的也会改变。如下图所示。
20200505183135.png

深克隆

要解决上面复制相同引用的问题,就要用到深拷贝。深拷贝实现的是对所有可变(没有被final修饰的引用变量)引用类型的成员变量都开辟内存空间所以一般深拷贝对于浅拷贝来说是比较耗费时间和内存开销的。

深拷贝有两种实现方法:

重写Clone()方法

Animal实现Cloneable接口

package cn.wuzheyi.clone1;

public class Animal implements Cloneable {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

    @Override
    public String toString() {
        return name;
    }
}

在Zoo中实现了Cloneable接口并重写了Clone()

package cn.wuzheyi.clone1;

public class Zoo implements Cloneable {
    private int number;
    private String name;
    private Animal[] animals;

    public Zoo() {

    }

    public Zoo(int number, String name) {
        this.number = number;
        this.name = name;
    }

    public Zoo(int number, String name, Animal[] animals) {
        this.number = number;
        this.name = name;
        this.animals = animals;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Animal[] getAnimals() {
        return animals;
    }

    public void setAnimals(Animal[] animals) {
        this.animals = animals;
    }

    @Override
    public String toString() {
        String description = number + " " + name;
        if (this.animals != null) {
            for (int i = 0; i < this.animals.length; i++) {
                description += " " + this.animals[i].getName();
            }
        }

        return description;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {

     Zoo zoo2 = (Zoo) super.clone(); if (this.animals != null &&
     this.animals.length > 0) { Animal[] animal2 = new
     Animal[this.animals.length]; for (int i = 0; i < this.animals.length; i++) {
     animal2[i] = (Animal) this.animals[i].clone(); } zoo2.setAnimals(animal2); }

     return zoo2;

//        return super.clone();
    }
}

执行和上面相同的测试代码后,控制台输出
QQ截图20200506093102.png
可以发现我们我们已经实现了深克隆,zoo2的引用已经指向不同的对象,所以不管怎么样修改zoo1,zoo2都不会发生改变。
20200505184927.png

序列化实现

通过重写Object的clone方法去实现深克隆十分麻烦,特别是嵌套比较多和有数组的情况下,重写Clone()很复杂。所以我们可以通过序列化实现深克隆

概念:

  • 序列化:把对象写到流里
  • 反序列化:把对象从流中读出来

在Java里深克隆一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。

注意:

  • 写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面
  • 对象以及对象内部所有引用到的对象都是可序列化的
  • 如果不想序列化,则需要使用transient来修饰

我们先实现DeepClone类,在类里面实现序列化和反序列化。

package cn.wuzheyi.clone2;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class DeepClone implements Serializable {

    private static final long serialVersionUID = -2658204965442453698L;

    protected Object deepClone() throws ClassNotFoundException, IOException {
        // 序列号
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(bos);

        oss.writeObject(this);

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }

}

让我们的对象都继承DeepClone方法,当然也就都实现了Serializable接口

package cn.wuzheyi.clone2;

public class Animal extends DeepClone {
    private static final long serialVersionUID = -293932665050190715L;

    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
package cn.wuzheyi.clone2;

public class Zoo extends DeepClone {
    private static final long serialVersionUID = -1812884732710635495L;

    private int number;
    private String name;
    private Animal[] animals;

    public Zoo() {

    }

    public Zoo(int number, String name) {
        this.number = number;
        this.name = name;
    }

    public Zoo(int number, String name, Animal[] animals) {
        this.number = number;
        this.name = name;
        this.animals = animals;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Animal[] getAnimals() {
        return animals;
    }

    public void setAnimals(Animal[] animals) {
        this.animals = animals;
    }

    @Override
    public String toString() {
        String description = number + " " + name;
        if (this.animals != null) {
            for (int i = 0; i < this.animals.length; i++) {
                description += " " + this.animals[i].getName();
            }
        }

        return description;
    }
}

再一次执行我们的测试类,得到的结果是相同的。
QQ截图20200506093102.png

总结

其实现在不推荐大家用Cloneable接口,实现比较麻烦,现在借助Apache Commons或者
springframework可以直接实现:

  • 浅克隆:BeanUtils.cloneBean(Object obj);BeanUtils.copyProperties(S,T);
  • 深克隆:SerializationUtils.clone(T object);

BeanUtils是利用反射原理获得所有类可见的属性和方法,然后复制到target类。
SerializationUtils.clone()就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization接口。


作者水平有限,有错误请指出,谢谢。


wuzheyi
10 声望0 粉丝

重庆邮电大学计算机学院2019级