前言

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


一、对象是如何存储的?

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

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

详情可见JVM基础

二、前置准备

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

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

四、浅拷贝

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

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

五、深拷贝

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

实现深拷贝有两种方式

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

Java 中的深拷贝和浅拷贝你了解吗?的更多相关文章

  1. 浅谈Java中的深拷贝和浅拷贝(转载)

    浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...

  2. 浅谈Java中的深拷贝和浅拷贝

    转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...

  3. 内功心法 -- Java中的深拷贝和浅拷贝

    写在前面的话:读书破万卷,编码如有神--------------------------------------------------------------------这篇博客主要来谈谈" ...

  4. Java中的深拷贝和浅拷贝

    1.浅拷贝与深拷贝概念 (1)浅拷贝(浅克隆) 浅拷贝又叫浅复制,将对象中的所有字段复制到新的对象(副本)中.其中,值类型字段(java中8中原始类型)的值被复制到副本中后,在副本中的修改不会影响到源 ...

  5. java中的深拷贝与浅拷贝

    Java中对象的创建 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那 ...

  6. Java中的深拷贝和浅拷贝(转载)

    深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java.虽然java自动管理对象的回收,但对于深拷贝(深复 ...

  7. java基础(十七)----- 浅谈Java中的深拷贝和浅拷贝 —— 面试必问

    假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short, ...

  8. 浅析Java中的深拷贝和浅拷

      浅析Java中的深拷贝和浅拷贝 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: in ...

  9. **Python中的深拷贝和浅拷贝详解

    Python中的深拷贝和浅拷贝详解   这篇文章主要介绍了Python中的深拷贝和浅拷贝详解,本文讲解了变量-对象-引用.可变对象-不可变对象.拷贝等内容.   要说清楚Python中的深浅拷贝,需要 ...

  10. C语言中的深拷贝和浅拷贝

    //C语言中的深拷贝和浅拷贝 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #inc ...

随机推荐

  1. 战“码”先锋直播预告丨如何成为一名优秀的OpenHamrony贡献者?

    OpenAtom OpenHarmony(以下简称"OpenHarmony")工作委员会首度发起「OpenHarmony开源贡献者计划」,旨在鼓励开发者参与OpenHarmony开 ...

  2. 王莉:将开发文档英文化和本地化,我们努力让OpenHarmony走向全球

    编者按:在 OpenHarmony 生态发展过程中,涌现了大批优秀的代码贡献者,本专题旨在表彰贡献.分享经验,文中内容来自嘉宾访谈,不代表 OpenHarmony 工作委员会观点. 王莉 华为技术有限 ...

  3. KL散度和交叉熵的对比介绍

    KL散度(Kullback-Leibler Divergence)和交叉熵(Cross Entropy)是在机器学习中广泛使用的概念.这两者都用于比较两个概率分布之间的相似性,但在一些方面,它们也有所 ...

  4. 在 macOS Big Sur 上安装 MySQL 8.x Compressed TAR Archive

    因个人能力有限,文章难免存在错误,望斧正,感谢. 版本: macOS 11.6 Intel版本 MySQL 8.0.26 Compressed TAR Archive 一.准备所需文件 前往 MySQ ...

  5. Apollo+ES源码改造,构建民生银行的ELK日志平台配置管理中心【转载】

    Apollo+ES源码改造,构建民生银行的ELK日志平台配置管理中心 原创 高效开发运维 架构头条 2019-02-28 作者 | 中国民生银行大数据基础平台运维组团队 编辑 | 张婵 随着 IT 业 ...

  6. Vue Mixin 的深入浅出

    mixin, 意为混入. 比如去买冰激凌,我先要一点奶油的,再来点香草的.我就可以吃一个奶油香草的冰激凌.如果再加点草莓,我可以同时吃三个口味的冰激凌. 代码表示 假设把你已有的奶油味的称为 base ...

  7. 力扣592(java)-分数加减运算(中等)

    题目: 给定一个表示分数加减运算的字符串 expression ,你需要返回一个字符串形式的计算结果. 这个结果应该是不可约分的分数,即最简分数. 如果最终结果是一个整数,例如 2,你需要将它转换成分 ...

  8. Helm Chart 多环境、多集群交付实践,透视资源拓扑和差异

    简介: 在本文中,我们将介绍如何通过 KubeVela解决多集群环境下 Helm Chart 的部署问题.如果你手里没有多集群也不要紧,我们将介绍一种仅依赖于 Docker 或者 Linux 系统的轻 ...

  9. 云效故障定位研究论文被ICSE 2021 SEIP track收录

    近期,由阿里云云效团队联合复旦大学CodeWisdom研究团队.阿里技术风险部安全生产团队,合作完成的论文<MicroHECL: High-Efficient Root Cause Locali ...

  10. HMS数据库设置和优化

    简介:Hive Metastore (HMS) 是一种服务,用于在后端 RDBMS(例如 MySQL 或 PostgreSQL)中存储与 Apache Hive 和其他服务相关的元数据.本文主要分享H ...