首页 > 编程语言 >Java 中的深拷贝和浅拷贝你了解吗?

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

时间:2024-05-21 11:07:14浏览次数:23  
标签:Animal Java name 对象 dog Dog 了解 animal 拷贝

前言

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


一、对象是如何存储的?

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

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

详情可见JVM基础

image.png

二、前置准备

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

@Data
@AllArgsConstructor
public class Animal{
    private int id;
    private String type;

    @Override
    public String toString () {
        return "Animal{" +
                "id=" + id +
                ", type='" + type + '\'' +
                '}';
    }
}

@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 +
                '}';
    }
}

三、直接赋值

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

下面我们进行代码验证:

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 的内存结构大致如下:

image.png

四、浅拷贝

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

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

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

@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 +
                '}';
    }
}

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

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 的内存结构大致如下:

image.png

五、深拷贝

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

实现深拷贝有两种方式

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

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

@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 + '\'' +
                '}';
    }
}
@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方法,结果如下。

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
@AllArgsConstructor
public class Animal implements Serializable {
    private int id;
    private String type;

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

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 的内存结构大致如下:

image.png

标签:Animal,Java,name,对象,dog,Dog,了解,animal,拷贝
From: https://www.cnblogs.com/fuxing/p/18203554

相关文章

  • JavaScript execute asynchronous functions in Parallel with count and Promise All
    JavaScriptexecuteasynchronousfunctionsinParallelwithcountandPromiseAllInOneJavaScript使用count和Promise并行执行异步函数errorsfunctionpromiseAll<T>(functions:Fn<T>[]):Promise<T[]>{returnnewPromise((resolve,reject)=&......
  • 实现“代码可视化”需要了解的前置知识-编译器中端
    1.前言前文实现“代码可视化”需要了解的前置知识-编译器前端介绍了编译器前端知识并附带了小练习,本文将继续介绍编译器中端相关的知识,还是概念+练习的学习方式。中间代码是用来进行程序分析和实现代码可视化的关键数据,了解其生成和优化方式能更好的帮助我们理解程序的执行逻辑,......
  • 在 JavaScript 中递归展开数组
    对嵌套数组使用递归:递归是处理嵌套数组的干净而有效的方法。它允许您处理任意深度的数组。使用该Array.isArray方法检查数组:这有助于确保代码适用于不同的数据类型并且更加健壮。 潜在性能问题:对大型数组要小心:处理非常深或很大的数组时,递归函数可能会导致堆栈溢出错误。在这......
  • Java开发Spring常见注解
    Java开发Spring常见注解  前言  Spring的一个核心功能是IOC,就是将Bean初始化加载到容器中,Bean是如何加载到容器的,可以使用Spring注解方式或者SpringXML配置方式。  注解本身没有功能的,就和xml一样。注解和xml都是一种元数据,元数据即解释数据的数据,这就是所谓配置......
  • 如何防止内部文档外泄,Ftrans文档安全外发系统了解一下!
    文档安全外发系统是一种用于保护企业或组织内部敏感信息不被未授权外泄的技术解决方案,比如内部文件、报告、数据等信息发送给外部的个人或组织,这个过程需要谨慎处理,确保信息安全。以下是一些常见的文档外发方式:电子邮件:通过电子邮件附件的形式发送文档是最常见的外发方式之一。......
  • 让机台数据传输更高效可靠,一文了解!
    在汽车制造业中,机台数据传输和管理是一个关键环节,它涉及到生产效率、产品质量和企业运营的多个方面。以下是一些机台数据传输和管理的关键点:车载通信技术:随着汽车智能化的提升,车载通信技术变得尤为重要。车内总线通信与车载无线通信技术的提升,使得智能电动汽车成为万物互联的新......
  • Java中CAS算法的集中体现:Atomic原子类库,你了解吗?
    一、写在开头在前面的博文中我们学习了volatile关键字,知道了它可以保证有序性和可见性,但无法保障原子性,结局原子性问题推荐使用synchronized、Lock或者AtomicInteger;我们还学习过CAS算法,在那篇博文中我们同样也提及atomic。那么今天,我们就来好好学一学Atomic原子库,一个基于CAS算......
  • 深入了解安全工具Vault、Vault根令牌和解封密钥,详细整理部署Vault的详细步骤
     一、深入了解安全工具VaultVault是一种开源工具,用于安全地存储、管理和控制访问各种机密信息,如密码、API令牌、安全配置和其他敏感数据。Vault使用强大的加密和安全管理技术来保护这些机密信息,并为应用程序和服务提供安全的访问控制机制。该工具支持各种云平台和技术堆栈,并提......
  • [Javascript] Perform Set Operations using JavaScript Set Methods
    The "SetMethods" proposalintroduces7newmethodstoJavaScript/ECMAScript:union(setB):ReturnsanewSetcontainingallelementsfromtheoriginalsetandanothersetsetB.Thismethodeffectivelycombinesthemembersoftwosetswithoutd......
  • Java项目包名该如何命名
    1.基本格式包名的一般格式是:com.公司名.项目名.模块名2.示例命名假设你的项目名称为XXX,可以考虑以下包名:(1)根包名:如果你有一个域名,例如example.com,则反向域名为com.example。根包名可以是com.example.XXX。(2)模块包名:根据......