• 浅拷贝、深拷贝与序列化【初级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();
        }
    }
    
    @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 接口只是一个标记接口(没属性和方法):

    public interface Cloneable {
    }
    

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

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

    测试类

    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跟着改变了

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

    深拷贝

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

    例子

    @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 接口。为的就是深拷贝的时候也能够克隆该字段。

    @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 也进行了克隆。

    来看测试类

    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 接口类似,都是标记型接口。

    来看例子

    @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 接口

    @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 进行反序列化,这样就实现了深拷贝。

    来看示例

    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() 方法差很多。

  • 相关阅读:
    2023-10学习笔记
    ELK 单机安装
    开源语言大模型演进史:早期革新
    Java安全—CommonsCollections4
    C++的运算符重载介绍
    解读C#编程中最容易忽略7种编写习惯!
    VS Code | 在VS Code中搭建你的R语言运行环境吧!~(图文介绍超详细)
    LeetCode 0264. 丑数 II
    设计模式-装饰者模式在Java中的使用示例
    2023考研常识之全国考研初试成绩基本分区要求是如何界定的?
  • 原文地址:https://www.cnblogs.com/GilbertDu/p/18245960