浅拷贝、深拷贝与序列化【初级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() 方法差很多。

相关推荐
此木|西贝4 小时前
【设计模式】享元模式
java·设计模式·享元模式
李少兄4 小时前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
bxlj_jcj5 小时前
JVM性能优化之年轻代参数设置
java·性能优化
八股文领域大手子5 小时前
深入理解缓存淘汰策略:LRU 与 LFU 算法详解及 Java 实现
java·数据库·算法·缓存·mybatis·哈希算法
不当菜虚困5 小时前
JAVA设计模式——(八)单例模式
java·单例模式·设计模式
m0_740154675 小时前
Maven概述
java·maven
吗喽对你问好6 小时前
Java位运算符大全
java·开发语言·位运算
Java致死6 小时前
工厂设计模式
java·设计模式·简单工厂模式·工厂方法模式·抽象工厂模式
程序员JerrySUN6 小时前
驱动开发硬核特训 · Day 21(上篇) 抽象理解 Linux 子系统:内核工程师的视角
java·linux·驱动开发