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

相关推荐
陈小桔13 分钟前
限流算法java实现
java
黑客老李17 分钟前
JavaSec | SpringAOP 链学习分析
java·运维·服务器·开发语言·学习·apache·memcached
勤奋的知更鸟43 分钟前
Java编程之原型模式
java·开发语言·原型模式
叶 落1 小时前
[Java 基础]数组
java·java 基础
KK溜了溜了1 小时前
JAVA-springboot log日志
java·spring boot·logback
珂朵莉MM1 小时前
2021 RoboCom 世界机器人开发者大赛-高职组(初赛)解题报告 | 珂学家
java·开发语言·人工智能·算法·职场和发展·机器人
香蕉炒肉1 小时前
Java优化:双重for循环
java·开发语言
黄雪超2 小时前
JVM——打开JVM后门的钥匙:反射机制
java·开发语言·jvm
有梦想的攻城狮2 小时前
spring中的@RabbitListener注解详解
java·后端·spring·rabbitlistener
李斯维2 小时前
循序渐进 Android Binder(二):传递自定义对象和 AIDL 回调
android·java·android studio