Java 轻松理解深拷贝与浅拷贝
前言
本文代码中有用到一些注解,主要是Lombok与junit用于简化代码。
主要是看到一堆代码会很乱,这样理解更清晰。如果没用过不用太过纠结。
对象的拷贝(克隆)是一个非常高频的操作,主要有以下三种方式:
- 直接赋值
- 拷贝:
- 浅拷贝
- 深拷贝
因为Java没有指针的概念,或者说是不需要我们去操心,这让我们省去了很多麻烦,但相应的,对于对象的引用、拷贝有时候就会有些懵逼,藏下一些很难发现的bug。
为了避免这些bug,理解这三种操作的作用与区别就是关键。
直接赋值
用等于号直接赋值是我们平时最常用的一种方式。
它的特点就是直接引用等号右边的对象
先来看下面的例子
先创建一个Person类
@Data
@AllArgsConstructor
@ToString
public class Person{
private String name;
private int age;
private Person friend;
}
测试
@Test
public void test() {
Person friend =new Person("老王",30,null);
Person person1 = new Person("张三", 20, null);
Person person2 = person1;
System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend(friend);
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}
结果
person1: Person(name=张三, age=20, friend=null)
person2: Person(name=张三, age=20, friend=null)
person1: Person(name=张四, age=25, friend=Person(name=老王, age=30, friend=null))
person2: Person(name=张四, age=25, friend=Person(name=老王, age=30, friend=null))
分析:
可以看到通过直接赋值进行拷贝,其实就只是单纯的对前对象进行引用。
如果这些对象都是基础对象当然没什么问题,但是如果对象进行操作,相当于两个对象同属一个实例。

拷贝
直接赋值虽然方便,但是很多时候并不是我们想要的结果,很多时候我们需要的是两个看似一样但是完全独立的两个对象。
这种时候我们就需要用到一个方法clone()
clone()并不是一个可以直接使用的方法,需要先实现Cloneable接口,然后重写它才能使用。
protected native Object clone() throws CloneNotSupportedException;
clone()方法被native关键字修饰,native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是系统或者其他语言来实现。
浅拷贝
浅拷贝可以实现对象克隆,但是存在一些缺陷。
定义:
- 如果原型对象的成员变量是值类型,将复制一份给克隆对象,也就是在堆中拥有独立的空间;
- 如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,指向相同的内存地址。
举例
光看定义不太好一下子理解,上代码看例子。
我们先来修改一下Person类,实现Cloneable接口,重写clone()方法,其实很简单,只需要用super调用一下即可
@Data
@AllArgsConstructor
@ToString
public class Person implements Cloneable {
private String name;
private int age;
private Friend friend;
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
-------
@Data
@AllArgsConstructor
public class Friend {
private String Name;
}
测试
@Test
public void test() {
Person person1 = new Person("张三", 20, "老王");
Person person2 = (Person) person1.clone();
System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend("小王");
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}
结果
person1: Person(name=张三, age=20, friend=Friend(Name=老王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))
person1: Person(name=张四, age=25, friend=Friend(Name=小王))
person2: Person(name=张三, age=20, friend=Friend(Name=小王))
可以看到,name age基本对象属性并没改变,而friend引用对象熟悉变了。
原理
Java浅拷贝的原理其实是把原对象的各个属性的地址拷贝给新对象。
注意我说的是各个属性,就算是基础对象属性其实也是拷贝的地址。
你可能有点晕了,都是拷贝了地址,为什么修改了 person1 对象的 name age 属性值,person2 对象的 name age 属性值没有改变呢?
我们一步步来,拿name属性来说明:
- String、Integer 等包装类都是不可变的对象
- 当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值
- 然后将原来的引用指向新的地址
- 我们修改了
person1对象的name属性值,person1对象的name字段指向了内存中新的String对象 - 我们并没有改变
person2对象的 name 字段的指向,所以person2对象的name还是指向内存中原来的String地址
看图

这个图已经很清晰的展示了其中的过程,因为person1 对象改变friend时是改变的引用对象的属性,并不是新建立了一个对象进行替换,原本老王的消失了,变成了小王。所以person2也跟着改变了。
深拷贝
深拷贝就是我们拷贝的初衷了,无论是值类型还是引用类型都会完完全全的拷贝一份,在内存中生成一个新的对象。
拷贝对象和被拷贝对象没有任何关系,互不影响。
深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

因为Java本身的特性,对于不可变的基本值类型,无论如何在内存中都是只有一份的。
所以对于不可变的基本值类型,深拷贝跟浅拷贝一样,不过并不影响什么。
实现:
想要实现深拷贝并不难,只需要在浅拷贝的基础上进行一点修改即可。
- 给friend添加一个
clone()方法。 - 在
Person类的clone()方法调用friend的clone()方法,将friend也复制一份即可。
@Data
@ToString
public class Person implements Cloneable {
private String name;
private int age;
private Friend friend;
public Person(String name, int age, String friend) {
this.name = name;
this.age = age;
this.friend = new Friend(friend);
}
public void setFriend(String friend) {
this.friend.setName(friend);
}
@Override
public Object clone() {
try {
Person person = (Person)super.clone();
person.friend = (Friend) friend.clone();
return person;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
------
@Data
@AllArgsConstructor
public class Friend implements Cloneable{
private String Name;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试
@Test
public void test() {
Person person1 = new Person("张三", 20, "老王");
Person person2 = (Person) person1.clone();
System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend("小王");
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}
结果
person1: Person(name=张三, age=20, friend=Friend(Name=老王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))
person1: Person(name=张四, age=25, friend=Friend(Name=小王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))
分析:
可以看到这次是真正的完全独立了起来。
需要注意的是,如果Friend类本身也存在引用类型,则需要在Friend类中的clone(),也去调用其引用类型的clone()方法,就如是Person类中那样,对!就是套娃!
所以对于存在多层依赖关系的对象,实现Cloneable接口重写clone()方法就显得有些笨拙了。
这里我们在介绍一种方法:利用序列化实现深拷贝
Serializable 实现深拷贝
修改Person和Friend,实现Serializable接口
@Data
@ToString
public class Person implements Serializable {
// ......同之前
public Object deepClone() throws Exception {
// 序列化
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();
}
}
---
@Data
@AllArgsConstructor
public class Friend implements Serializable {
private String Name;
}
测试
@Test
public void test() {
Person person1 = new Person("张三", 20, "老王");
Person person2 = null;
try {
person2 = (Person) person1.deepClone();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend("小王");
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}
结果
person1: Person(name=张三, age=20, friend=Friend(Name=老王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))
person1: Person(name=张四, age=25, friend=Friend(Name=小王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))
只要将会被复制到的引用对象标记Serializable接口,通过序列化到方式即可实现深拷贝。
原理:
对象被序列化成流后,因为写在流里的是对象的一个拷贝,而原对象仍然存在于虚拟机里面。
通过反序列化就可以获得一个完全相同的拷贝。
利用这个特性就实现了对象的深拷贝。
总结
- 直接赋值是将新的对象指向原对象所指向的实例,所以一旦有所修改,两个对象会一起变。
- 浅拷贝是把原对象属性的地址传给新对象,对于不可变的基础类型,实现了二者的分离,但对于引用对象,二者还是会一起改变。
- 深拷贝是真正的完全拷贝,二者没有关系。实现深拷贝时如果存在多层依赖关系,可以采用序列化的方式来进行实现。
对于
Serializable接口、Cloneable接口,其实都是相当于一个标记,点进去看源码,其实他们是一个空接口。
Java 轻松理解深拷贝与浅拷贝的更多相关文章
- java克隆之深拷贝与浅拷贝
版权声明:本文出自汪磊的博客,转载请务必注明出处. Java深拷贝与浅拷贝实际项目中用的不多,但是对于理解Java中值传递,引用传递十分重要,同时个人认为对于理解内存模型也有帮助,况且面试中也是经常问 ...
- 浅谈Java中的深拷贝和浅拷贝(转载)
浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...
- 浅谈Java中的深拷贝和浅拷贝
转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...
- 内功心法 -- Java中的深拷贝和浅拷贝
写在前面的话:读书破万卷,编码如有神--------------------------------------------------------------------这篇博客主要来谈谈" ...
- Java基础(十三)--深拷贝和浅拷贝
在上篇文章:Java基础(十二)--clone()方法,我们简单介绍了clone()的使用 clone()对于基本数据类型的拷贝是完全没问题的,但是如果是引用数据类型呢? @Data @NoArgsC ...
- java中的深拷贝与浅拷贝
Java中对象的创建 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那 ...
- Java中的深拷贝和浅拷贝
1.浅拷贝与深拷贝概念 (1)浅拷贝(浅克隆) 浅拷贝又叫浅复制,将对象中的所有字段复制到新的对象(副本)中.其中,值类型字段(java中8中原始类型)的值被复制到副本中后,在副本中的修改不会影响到源 ...
- Java中的深拷贝和浅拷贝(转载)
深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java.虽然java自动管理对象的回收,但对于深拷贝(深复 ...
- Java对象的深拷贝和浅拷贝、集合的交集并集
http://blog.csdn.net/lian_1988/article/details/45970927 http://www.cnblogs.com/yxnchinahlj/archive/2 ...
随机推荐
- Java RPC 框架 Solon 1.3.9 发布,更便利的支持
Solon 是一个微型的Java RPC开发框架.项目从2018年启动以来,参考过大量前人作品:历时两年,4000多次的commit:内核保持0.1m的身材,超高的跑分,良好的使用体验.支持:RPC. ...
- C# ref and out
相同点: 1. ref 和 out 都是按地址传递的,使用后都将改变原来参数的数值: 2. 方法定义和调用方法都必须显式使用 ref 或者 out关键字: 3. 通过ref 和 ref 特性,一定程度 ...
- HDOJ-2087(KMP算法)
剪花布条 HDOJ-2087 本题和hdoj-1686相似,唯一不同的是这里的子串一定要是单独的.所以在确定有多少个子串时不能用前面的方法.而是在循环时,只要找到一个子串,i就不是++,而是+=子串的 ...
- 通达OA 越权访问-2013/2015版本
漏洞参考 http://wiki.0-sec.org/0day/%E9%80%9A%E8%BE%BEoa/9.html 复现 根据⽹上的通达 OA的源码找这些敏感地址,如: /general/syst ...
- C# 应用 - 使用 HttpWebRequest 发起 Http 请求
helper 类封装 调用 1. 引用的库类 \Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.dll Syste ...
- Java 在PPT中添加文本水印的简易方法(单一/平铺水印)
[前言] 在PPT幻灯片中,可通过添加形状的方式,来实现类似水印的效果,可添加单一文本水印效果,即在幻灯片中心位置水印以单个文本字样显示,但通过一定方法也可以添加多行(平铺)文本水印效果,即在幻灯片中 ...
- IPFS挖矿原理介绍
随着近几年区块链行业迅速发展,虚拟货币交易机制逐渐成熟,作为「区块链新贵」的 IPFS渐渐走入广大投资者的视线. IPFS 与其激励层的运作原理是投资者们必须要了解的.所以今天我就来和大家讲讲 IPF ...
- wrf模拟的domain图绘制
wrf模拟的区域绘制,domain图,利用python的cartopy库绘制模拟区域 参考Liang Chen的draw_wrf_domian.py这个代码, 出处python画wrf模式的模拟区域 ...
- od快捷键
视图.查看相关: Alt+l 记录 Alt+e 可执行模块 Alt+m 内存 Alt+c cpu(反汇编视图) Ctrl+p 补丁 Alt+k 调用堆栈 Alt+b 断点 Alt+f5 设置窗口总在 ...
- Java中BO、DAO、DO、DTO、PO、POJO、VO的概念
在程序开发中,经常会碰到各种专业术语,这里统一做一下解释,有遗漏或理解错误的恳请指正. BO(Business Object)业务对象 主要作用是把业务逻辑封装为一个对象,这个对象可以包括一个或多个其 ...