JAVA对象的拷贝/克隆(“浅拷贝、深拷贝“) 浅析

大型纪录片《养猫传奇》正在为您播出:

要将一个实例化对象拷贝给另一个刚创建的同类型的实例化对象,可以使用以下俩种方法:

第一种方法:

  1.     实现 Cloneable 接口并重写 clone() 方法

    • 在要进行拷贝的类(这里是Test类)中实现 Cloneable 接口。
    • 重写 clone() 方法,调用父类的 clone() 方法,并进行必要的字段赋值。
    • 使用 clone() 方法创建新对象并将原始对象的属性复制到新对象中。

先看示例吧:

class Test implements Cloneable{//测试类test实现Cloneable接口

    private Cat cat = new Cat("momo");//待测试属性,默认设置为momo

    public String getCatName(){
        System.out.println(cat.getName());
        return cat.getName();
    }

    public void setCatName(String A){ this.cat.setName(A); }

    @Override//重写父类Object的 clone() 方法
    protected Test clone() throws CloneNotSupportedException{
       return (Test)super.clone();
    };
}

        不难看出,测试类是要实现接口Cloneable的,并且定义了一个Cat属性变量cat,下面看下关联类Cat吧!

class Cat implements Animal{//这里接口不重要,因为有强迫症所以不想删!!
    private String name;//猫的名字,后续测试使用
    public Cat(){};

    public Cat(String Name){
        this.name = Name;
    }

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

    public String getName(){ return this.name; }

}

        在Test类中重写 clone()方法时,调用 super.clone() 是指调用父类(隐式显示Object clone() 方法,它是实现对象拷贝的基础。 Object 类中的 clone() 方法是一个受保护的方法,它执行对象的浅拷贝。当你在子类中重写 该方法时,通常会先调用父类的 clone() 方法来创建一个对象副本,并且该副本具有与原始对象相同的字段值。

        使用 `super.clone()` 的目的是利用默认实现进行浅拷贝,并确保返回正确类型(即将其强制转换为子类类型)。然后,在新创建的对象上可以进行进一步处理和修改以满足特定需求。

        浅拷贝虽然是好用但是是有缺点的,让我们看看为啥(测试结果)吧!

public static void main(String[] args) throws CloneNotSupportedException {
        test a = new test();
        test b = a.clone();//将a浅拷贝给b
        
        //重点放在引用类型拷贝的测试上,这里当然就是指Test类型的对象b中Cat的拷贝情况了~

        a.getCatName();//每个Test对象的猫猫是有初始名字"momo"的,忘记了可以看看下Test类
        b.getCatName();
        b.setCatName("yiyi");//将b的猫猫的名字改为yiyi,看是否改变了a中猫猫的名字
        a.getCatName();
        b.getCatName();
}

        由测试结果我们可以看出,“神”先借着a对象创造了一个b对象,这里改完b对象的猫猫的名字后,a的猫猫也改变了对应的名字。这说明b和a这两个测试对象养的居然是同一只猫猫啊!(这太可怕了吧!),其实这就是因为浅拷贝不支持引用类型的成员变量的赋值的,这里实际上就是简单的吧a的猫猫的地址给了b,完事儿俩人养了同一只猫......

        所以需要注意的是,默认情况下,`Object` 类中提供的 `clone()` 方法执行浅拷贝(不支持引用类型的成员变量的复制)。如果需要进行深度拷贝(即复制包含引用类型字段所引用对象),则需要进一步处理(其实就是第二种方法了~)。这涉及到递归地对所有引用类型字段进行克隆操作或使用其他方式来复制相关内容。



第二种方法:

实现 Serializable 接口并使用序列化/反序列化:

  • 在要进行拷贝的类中实现 Serializable 接口。
  • 将原始对象通过序列化转换为字节流。
  • 再从字节流中反序列化成新创建的对象。

下面是实例:

这里是测试类Test,实现了接口Serializable,并且实现了一个自定义方法cloneMin()用于深拷贝

class Test implements Serializable {//同理实现接口

    private Cat cat = new Cat("momo");//设置默认值为momo

    public String getCatName(){
        System.out.println(cat.getName());
        return cat.getName();
    }
    public void setCatName(String A){ this.cat.setName(A); }

//    @Override//重写父类Object的 clone() 方法
//    protected test clone() throws CloneNotSupportedException{
//       return (test)super.clone();
//    };//这是上一个方法,这里我给注释掉了,无碍

    public Test cloneMin(){
        Test copy = null;//创建待拷贝副本
        try {
            // 使用序列化和反序列化进行拷贝
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);

            oos.writeObject(this); // 将原始对象写入字节流

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            copy = (Test) ois.readObject(); // 从字节流中读取对象

            // 现在 copy 是 original 的副本,两个对象是独立的
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return copy;
    }

}

下面是关联类Cat,也实现了接口Serializable

class Cat implements Animal,Serializable{//实现接口Serializable

    private String name;//待测试属性

    public Cat(){};

    public Cat(String Name){
        this.name = Name;
    }

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

    public String getName(){ return this.name; }
    

}
public class Moyi {

    public static void main(String[] args) throws CloneNotSupportedException {
        Test a = new Test();
        Test b = a.cloneMin();//使用自定义拷贝函数cloneMin()将a(深)拷贝给b

        a.getCatName();
        b.getCatName();
        b.setCatName("yiyi");
        a.getCatName();
        b.getCatName();
}

测试的步骤和第一个方法是一样的,但是结果却是不一样的哦!!

再经过测试得到的结果就是下面的了:

        根据结果我们可以以看出,“神”先借着a对象创造了一个b对象,这里改完b对象的猫猫的名字为“yiyi”后,a的猫猫并没有改变名字还是"momo"。由此,发现使用序列化/反序列化的方法是属于深拷贝的(即复制包含引用类型字段所引用对象)。a,b俩人终于不是养的同一只猫了,而是两只不一样的!

        最后的最后,可以给作者一个小小的却又大大的赞么,谢谢各位了!