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

前言

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


一、对象是如何存储的?

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

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

详情可见JVM基础

二、前置准备

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

java 复制代码
@Data
@AllArgsConstructor
public class Animal{
    private int id;
    private String type;

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

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

三、直接赋值

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

下面我们进行代码验证:

java 复制代码
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));
    }
}
basic 复制代码
两个对象是否相等:true
----------------------------
变化后两个对象是否相等:true

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

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

四、浅拷贝

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

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

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

java 复制代码
@Data
@AllArgsConstructor
public 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 +
                '}';
    }
}

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

java 复制代码
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()));
    }
java 复制代码
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()方法

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

java 复制代码
@Data
@AllArgsConstructor
public 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 + '\'' +
                '}';
    }
}
java 复制代码
@Data
@AllArgsConstructor
public 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方法,结果如下。

java 复制代码
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.序列化

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

java 复制代码
@Data
@AllArgsConstructor
public class Animal implements Serializable {
    private int id;
    private String type;

    @Override
    public String toString () {
        return "Animal{" +
                "id=" + id +
                ", type='" + type + '\'' +
                '}';
    }
}
java 复制代码
@Data
@AllArgsConstructor
public 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方法,结果如下。

java 复制代码
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 的内存结构大致如下:

相关推荐
吴冰_hogan4 小时前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
东阳马生架构12 小时前
JVM实战—1.Java代码的运行原理
jvm
ThisIsClark15 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉15 小时前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥17 小时前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开17 小时前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构2 天前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥2 天前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇2 天前
JavaAgent技术应用和原理:JVM持久化监控
jvm