浅拷贝、深拷贝与序列化【初级Java必需理解的概念】

浅拷贝

首先创建两个类,方便理解浅拷贝

java 复制代码
@Data
class Student implements Cloneable{
    //年龄和名字是基本属性
    private int age;
    private String name;
    //书包是引用属性
    private Bag bag;

    public Student(int age, String name, Bag bag) {
        this.age = age;
        this.name = name;
        this.bag = bag;
    }

    @Override
    public String toString() {
        return "age=" + age + ", name='" + name + ", bag=" + bag;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
java 复制代码
@Data
class Bag {
    private String color;
    private int price;

    public Bag(String color, int price) {
        this.color = color;
        this.price = price;
    }

    @Override
    public String toString() {
        return "color='" + color + ", price=" + price;
    }
}

Cloneable 接口只是一个标记接口(没属性和方法):

java 复制代码
public interface Cloneable {
}

标记接口的作用其实很简单,用来表示某个功能在执行的时候是合法的。

如果不实现Cloneable接口直接重写并调用clone()方法,会抛出 CloneNotSupportedException 异常。

测试类

java 复制代码
class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student(18, "张三", new Bag("红",100));
        Student student2 = (Student) student1.clone();

        System.out.println("浅拷贝后:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

        //修改非引用类型属性name
        student2.setName("李四");

        //修改引用类型属性bag
        Bag bag = student2.getBag();
        bag.setColor("蓝");
        bag.setPrice(200);

        System.out.println("修改了 student2 的 name 和 bag 后:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

    }
}

//打印结果
浅拷贝后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='张三, bag=color='红, price=100
修改了 student2 的 name 和 bag 后:
student1:age=18, name='张三, bag=color='蓝, price=200
student2:age=18, name='李四, bag=color='蓝, price=200

可以看得出,浅拷贝后:

修改了student2的非引用类型属性name,student1的name并不会跟着改变

但修改了student2的引用类型属性bag,student1的bag跟着改变了

说明浅拷贝克隆的对象中,引用类型的字段指向的是同一个,当改变任何一个对象,另外一个对象也会随之改变。

深拷贝

深拷贝和浅拷贝不同的,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象,另外一个对象不会随之改变。

例子

java 复制代码
@Data
class Bag implements Cloneable {
    private String color;
    private int price;

    public Bag(String color, int price) {
        this.color = color;
        this.price = price;
    }

    @Override
    public String toString() {
        return "color='" + color + ", price=" + price;
    }

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

注意,此时的 Bag 类和浅拷贝时不同,重写了 clone() 方法,并实现了 Cloneable 接口。为的就是深拷贝的时候也能够克隆该字段。

java 复制代码
@Data
class Student implements Cloneable{
    //年龄和名字是基本属性
    private int age;
    private String name;
    //书包是引用属性
    private Bag bag;

    public Student(int age, String name, Bag bag) {
        this.age = age;
        this.name = name;
        this.bag = bag;
    }

    @Override
    public String toString() {
        return "age=" + age + ", name='" + name + ", bag=" + bag;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student s = (Student) super.clone();
        s.setBag((Bag) s.getBag().clone());
        return s;
    }

}

注意,此时 Student 类也与之前的不同,clone() 方法当中,不再只调用 Object 的 clone() 方法对 Student 进行克隆了,还对 Bag 也进行了克隆。

来看测试类

java 复制代码
class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student(18, "张三", new Bag("红",100));
        Student student2 = (Student) student1.clone();

        System.out.println("深拷贝后:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

        //修改非引用类型属性name
        student2.setName("李四");

        //修改引用类型属性bag
        Bag bag = student2.getBag();
        bag.setColor("蓝");
        bag.setPrice(200);

        System.out.println("修改了 student2 的 name 和 bag 后:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);
    }
}

//这个测试类和之前的浅拷贝的测试类一样,但运行结果是不同的。
深拷贝后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='张三, bag=color='红, price=100
修改了 student2 的 name 和 bag 后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='李四, bag=color='蓝, price=200

不只是 student1 和 student2 是不同的对象,它们中的 bag 也是不同的对象。所以,改变了 student2 中的 bag 并不会影响到 student1。

不过,通过 clone() 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 clone() 方法。

更好的方法是利用序列化

序列化

序列化是将对象写入流中,而反序列化是将对象从流中读取出来。写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。

来看例子

java 复制代码
@Data
class Bag implements Serializable {
    private String color;
    private int price;

    public Bag(String color, int price) {
        this.color = color;
        this.price = price;
    }

    @Override
    public String toString() {
        return "color='" + color + ", price=" + price;
    }
}

Bag 需要实现 Serializable 接口

java 复制代码
@Data
class Student implements Serializable {
    //年龄和名字是基本属性
    private int age;
    private String name;
    //书包是引用属性
    private Bag bag;

    public Student(int age, String name, Bag bag) {
        this.age = age;
        this.name = name;
        this.bag = bag;
    }

    @Override
    public String toString() {
        return "age=" + age + ", name='" + name + ", bag=" + bag;
    }

    //使用序列化拷贝
    public Object serializeClone() throws IOException, ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);

        oos.writeObject(this);

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

        return ois.readObject();
    }

}

Student 类也需要实现 Serializable 接口,并且在该类中,增加了一个 serializeClone() 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。

来看示例

java 复制代码
class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Student student1 = new Student(18, "张三", new Bag("红",100));
        Student student2 = (Student) student1.serializeClone();

        System.out.println("浅拷贝后:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

        //修改非引用类型属性name
        student2.setName("李四");

        //修改引用类型属性bag
        Bag bag = student2.getBag();
        bag.setColor("蓝");
        bag.setPrice(200);

        System.out.println("修改了 student2 的 name 和 bag 后:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

    }
}


//与之前测试类不同的是,调用了 serializeClone() 方法。
浅拷贝后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='张三, bag=color='红, price=100
修改了 student2 的 name 和 bag 后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='李四, bag=color='蓝, price=200

测试结果和之前用 clone() 方法实现的深拷贝一样。

clone() 方法同时是一个本地(native)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。

需要注意,由于是序列化涉及到输入流和输出流的读写,在性能上要比 HotSpot 虚拟机实现的 clone() 方法差很多。

相关推荐
2401_8532757317 分钟前
ArrayList 源码分析
java·开发语言
爪哇学长21 分钟前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
MoFe135 分钟前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
_江南一点雨44 分钟前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
深情废杨杨1 小时前
后端-实现excel的导出功能(超详细讲解)
java·spring boot·excel
智汇探长1 小时前
EasyExcel自定义设置Excel表格宽高
java·excel·easyexcel
酸奶代码1 小时前
Spring AOP技术
java·后端·spring
代码小鑫1 小时前
A034-基于Spring Boot的供应商管理系统的设计与实现
java·开发语言·spring boot·后端·spring·毕业设计
paopaokaka_luck1 小时前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法
guoruijun_2012_42 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言