写点什么

Java 中的深拷贝和浅拷贝你了解吗?

  • 2024-05-22
    福建
  • 本文字数:4225 字

    阅读完需:约 14 分钟

前言


Java 开发中,对象拷贝是常有的事,很多人可能搞不清到底是拷贝了引用还是拷贝了对象。本文将详细介绍相关知识,让你充分理解 Java 拷贝。


一、对象是如何存储的?


方法执行过程中,方法体中的数据类型主要分两种,它们的存储方式是不同的(如下图):


  1. 基本数据类型: 直接存储在栈帧的局部变量表中;

  2. 引用数据类型: 对象的引用存储在栈帧的局部变量表中,而对实例本身及其所有成员变量存放在堆内存中。


详情可见JVM基础



二、前置准备


创建两个实体类方便后续的代码示例

@Data@AllArgsConstructorpublic class Animal{    private int id;    private String type;     @Override    public String toString () {        return "Animal{" +                "id=" + id +                ", type='" + type + '\'' +                '}';    }}
复制代码


@Data@AllArgsConstructorpublic class Dog {    private int age;    private String name;    private Animal animal;     @Override    public String toString () {        return "Dog{" +                "age=" + age +                ", name='" + name + '\'' +                ", animal=" + animal +                '}';    }}
复制代码


三、直接赋值


直接赋值是我们最常用的方式,它只是拷贝了对象引用地址,并没有在内存中生成新的对象


下面我们进行代码验证:

public class FuXing {    public static void main (String[] args) {        Animal animal = new Animal(1, "dog");        Dog dog = new Dog(18, "husky", animal);        Dog dog2 = dog;        System.out.println("两个对象是否相等:" + (dog2 == dog));         System.out.println("----------------------------");        dog.setAge(3);        System.out.println("变化后两个对象是否相等:" + (dog2 == dog));    }}
复制代码


两个对象是否相等:true----------------------------变化后两个对象是否相等:true
复制代码


通过运行结果可知,dog类的age已经发生变化,但重新打印两个类依然相等。所以它只是拷贝了对象引用地址,并没有在内存中生成新的对象


直接赋值的 JVM 的内存结构大致如下:



四、浅拷贝


浅拷贝后会创建一个新的对象,且新对象的属性和原对象相同。但是,拷贝时针对原对象的属性的数据类型的不同,有两种不同的情况:


  1. 属性的数据类型基本类型,拷贝的就是基本类型的值;

  2. 属性的数据类型引用类型,拷贝的就是对象的引用地址,意思就是拷贝对象与原对象引用同一个对象


要实现对象浅拷贝还是比较简单的,只需要被拷贝的类实现Cloneable接口,重写clone方法即可。下面我们对Dog进行改动:

@Data@AllArgsConstructorpublic class Dog implements Cloneable{    private int age;    private String name;    private Animal animal;     @Override    public Dog clone () throws CloneNotSupportedException {        return (Dog) super.clone();    }     @Override    public String toString () {        return "Dog{" +                "age=" + age +                ", name='" + name + '\'' +                ", animal=" + animal +                '}';    }}
复制代码


接下来我们运行下面的代码,看一下运行结果:

public class FuXing {    public static void main (String[] args) throws Exception {        Animal animal = new Animal(1, "dog");        Dog dog = new Dog(18, "husky", animal);         // 克隆对象        Dog cloneDog = dog.clone();         System.out.println("dog:" + dog);        System.out.println("cloneDog:" + cloneDog);        System.out.println("两个对象是否相等:" + (cloneDog == dog));        System.out.println("两个name是否相等:" + (cloneDog.getName() == dog.getName()));        System.out.println("两个animal是否相等:" + (cloneDog.getAnimal() == dog.getAnimal()));         System.out.println("----------------------------------------");         // 更改原对象的属性值        dog.setAge(3);        dog.setName("corgi");        dog.getAnimal().setId(2);         System.out.println("dog:" + dog);        System.out.println("cloneDog:" + cloneDog);        System.out.println("两个对象是否相等:" + (cloneDog == dog));        System.out.println("两个name是否相等:" + (cloneDog.getName() == dog.getName()));        System.out.println("两个animal是否相等:" + (cloneDog.getAnimal() == dog.getAnimal()));    }
复制代码


dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}两个对象是否相等:false两个name是否相等:true两个animal是否相等:true----------------------------------------dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}}cloneDog:Dog{age=18, name='husky', animal=Animal{id=2, type='dog'}}两个对象是否相等:false两个name是否相等:false两个animal是否相等:true
复制代码


我们分析下运行结果,重点看一下 “两个 name 是否相等”,改动后变成 false.

这是因为StringInteger等包装类都是不可变的对象,当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值,然后将原来的引用指向新的地址


这里dog对象的name属性已经指向一个新的对象,而cloneDogname属性仍然指向原来的对象,所以就不同了。


然后我们看下两个对象的animal属性,原对象属性值变动后,拷贝对象也跟着变动,这就是因为拷贝对象与原对象引用同一个对象


浅拷贝的 JVM 的内存结构大致如下:



五、深拷贝


与浅拷贝不同之处,深拷贝在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且拷贝其成员变量。也就是说,深拷贝出来的对象,与原对象没有任何关联,是一个新的对象。


实现深拷贝有两种方式


1. 让每个引用类型属性都重写 clone()方法


注意: 这里如果引用类型的属性或者层数太多了,代码量会变很大,所以一般不建议使用

@Data@AllArgsConstructorpublic class Animal implements Cloneable{    private int id;    private String type;     @Override    protected Animal clone () throws CloneNotSupportedException {        return (Animal) super.clone();    }     @Override    public String toString () {        return "Animal{" +                "id=" + id +                ", type='" + type + '\'' +                '}';    }}
复制代码


@Data@AllArgsConstructorpublic class Dog implements Cloneable{    private int age;    private String name;    private Animal animal;     @Override    public Dog clone () throws CloneNotSupportedException {        Dog clone = (Dog) super.clone();        clone.animal = animal.clone();        return clone;    }     @Override    public String toString () {        return "Dog{" +                "age=" + age +                ", name='" + name + '\'' +                ", animal=" + animal +                '}';    }}
复制代码


我们再次运行浅拷贝部分的main方法,结果如下。

dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}两个对象是否相等:false两个name是否相等:true两个animal是否相等:false # 变为false----------------------------------------dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}}cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}两个对象是否相等:false两个name是否相等:false两个animal是否相等:false # 变为false
复制代码


2.序列化


序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。


@Data@AllArgsConstructorpublic class Animal implements Serializable {    private int id;    private String type;     @Override    public String toString () {        return "Animal{" +                "id=" + id +                ", type='" + type + '\'' +                '}';    }}
复制代码


@Data@AllArgsConstructorpublic class Dog implements Serializable {    private int age;    private String name;    private Animal animal;     @SneakyThrows    @Override    public Dog clone () {        // 序列化        ByteArrayOutputStream bos = new ByteArrayOutputStream();        ObjectOutputStream oos = new ObjectOutputStream(bos);        oos.writeObject(this);         //反序列化        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());        ObjectInputStream ois = new ObjectInputStream(bis);        return (Dog) ois.readObject();    }     @Override    public String toString () {        return "Dog{" +                "age=" + age +                ", name='" + name + '\'' +                ", animal=" + animal +                '}';    }}
复制代码


我们再次运行浅拷贝部分的main方法,结果如下。

dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}两个对象是否相等:false两个name是否相等:false # 变为false两个animal是否相等:false # 变为false----------------------------------------dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}}cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}}两个对象是否相等:false两个name是否相等:false两个animal是否相等:false # 变为false
复制代码


深拷贝的 JVM 的内存结构大致如下:



文章转载自:fuxing.

原文链接:https://www.cnblogs.com/fuxing/p/18203554

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Java 中的深拷贝和浅拷贝你了解吗?_Java_快乐非自愿限量之名_InfoQ写作社区