Java深拷贝和浅拷贝

25次阅读

共计 12500 个字符,预计需要花费 32 分钟才能阅读完成。

目录介绍

01. 对象拷贝有哪些

02. 理解浅拷贝

2.1 什么是浅拷贝
2.2 实现浅拷贝案例

03. 理解深拷贝

3.1 什么是深拷贝
3.2 实现深拷贝案例

04. 序列化进行拷贝

4.1 序列化属于深拷贝
4.2 注意要点
4.3 序列化案例

05. 延迟拷贝
06. 如何选择拷贝方式

07. 数组的拷贝

7.1 基本数据类型数组
7.2 引用数据类型数组

08. 集合的拷贝

8.1 集合浅拷贝
8.2 集合深拷贝

好消息

博客笔记大汇总【16 年 3 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 N 篇[近 100 万字,陆续搬到网上],转载请注明出处,谢谢!
链接地址:https://github.com/yangchong2…
如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01. 对象拷贝有哪些

对象拷贝 (Object Copy) 就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部数据。
Java 中有三种类型的对象拷贝:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)、延迟拷贝(Lazy Copy)。

02. 理解浅拷贝
2.1 什么是浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

在上图中,SourceObject 有一个 int 类型的属性 “field1″ 和一个引用类型属性 ”refObj”(引用 ContainedObject 类型的对象)。当对 SourceObject 做浅拷贝时,创建了 CopiedObject,它有一个包含 ”field1″ 拷贝值的属性 ”field2″ 以及仍指向 refObj 本身的引用。由于 ”field1″ 是基本类型,所以只是将它的值拷贝给 ”field2″,但是由于 ”refObj” 是一个引用类型, 所以 CopiedObject 指向 ”refObj” 相同的地址。因此对 SourceObject 中的 ”refObj” 所做的任何改变都会影响到 CopiedObject。

2.2 如何实现浅拷贝

下面来看一看实现浅拷贝的一个例子
public class Subject {

private String name;
public Subject(String s) {
name = s;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}
}
public class Student implements Cloneable {

// 对象引用
private Subject subj;
private String name;

public Student(String s, String sub) {
name = s;
subj = new Subject(sub);
}

public Subject getSubj() {
return subj;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}

/**
* 重写 clone()方法

*/
public Object clone() {
// 浅拷贝
try {
// 直接调用父类的 clone()方法
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
“`

“`
private void test1(){
// 原始对象
Student stud = new Student(“ 杨充 ”, “ 潇湘剑雨 ”);
System.out.println(“ 原始对象: ” + stud.getName() + ” – ” + stud.getSubj().getName());

// 拷贝对象
Student clonedStud = (Student) stud.clone();
System.out.println(“ 拷贝对象: ” + clonedStud.getName() + ” – ” + clonedStud.getSubj().getName());

// 原始对象和拷贝对象是否一样:
System.out.println(“ 原始对象和拷贝对象是否一样: ” + (stud == clonedStud));
// 原始对象和拷贝对象的 name 属性是否一样
System.out.println(“ 原始对象和拷贝对象的 name 属性是否一样: ” + (stud.getName() == clonedStud.getName()));
// 原始对象和拷贝对象的 subj 属性是否一样
System.out.println(“ 原始对象和拷贝对象的 subj 属性是否一样: ” + (stud.getSubj() == clonedStud.getSubj()));

stud.setName(“ 小杨逗比 ”);
stud.getSubj().setName(“ 潇湘剑雨大侠 ”);
System.out.println(“ 更新后的原始对象: ” + stud.getName() + ” – ” + stud.getSubj().getName());
System.out.println(“ 更新原始对象后的克隆对象: ” + clonedStud.getName() + ” – ” + clonedStud.getSubj().getName());
}
“`

输出结果如下:
2019-03-23 13:50:57.518 24704-24704/com.ycbjie.other I/System.out: 原始对象: 杨充 – 潇湘剑雨
2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 拷贝对象: 杨充 – 潇湘剑雨
2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象是否一样: false
2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象的 name 属性是否一样: true
2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象的 subj 属性是否一样: true
2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新后的原始对象: 小杨逗比 – 潇湘剑雨大侠
2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新原始对象后的克隆对象: 杨充 – 潇湘剑雨大侠

可以得出的结论
在这个例子中,让要拷贝的类 Student 实现了 Clonable 接口并重写 Object 类的 clone()方法,然后在方法内部调用 super.clone()方法。从输出结果中我们可以看到,对原始对象 stud 的 ”name” 属性所做的改变并没有影响到拷贝对象 clonedStud,但是对引用对象 subj 的 ”name” 属性所做的改变影响到了拷贝对象 clonedStud。

03. 理解深拷贝
3.1 什么是深拷贝

深拷贝会拷贝所有的属性, 并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

在上图中,SourceObject 有一个 int 类型的属性 “field1″ 和一个引用类型属性 ”refObj1″(引用 ContainedObject 类型的对象)。当对 SourceObject 做深拷贝时,创建了 CopiedObject,它有一个包含 ”field1″ 拷贝值的属性 ”field2″ 以及包含 ”refObj1″ 拷贝值的引用类型属性 ”refObj2”。因此对 SourceObject 中的 ”refObj” 所做的任何改变都不会影响到 CopiedObject

3.2 实现深拷贝案例

下面是实现深拷贝的一个例子。只是在浅拷贝的例子上做了一点小改动,Subject 和 CopyTest 类都没有变化。
public class Student implements Cloneable {
// 对象引用
private Subject subj;
private String name;

public Student(String s, String sub) {
name = s;
subj = new Subject(sub);
}

public Subject getSubj() {
return subj;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}

/**
* 重写 clone()方法
*

*/
public Object clone() {
// 深拷贝,创建拷贝类的一个新对象,这样就和原始对象相互独立
Student s = new Student(name, subj.getName());
return s;
}
}
“`

输出结果如下:
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象: 杨充 – 潇湘剑雨
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 拷贝对象: 杨充 – 潇湘剑雨
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象是否一样: false
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象的 name 属性是否一样: true
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象的 subj 属性是否一样: false
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新后的原始对象: 小杨逗比 – 潇湘剑雨大侠
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新原始对象后的克隆对象: 杨充 – 潇湘剑雨

得出的结论
很容易发现 clone()方法中的一点变化。因为它是深拷贝,所以你需要创建拷贝类的一个对象。因为在 Student 类中有对象引用,所以需要在 Student 类中实现 Cloneable 接口并且重写 clone 方法。

04. 序列化进行拷贝
4.1 序列化属于深拷贝
可能你会问,序列化是属于那种类型拷贝?答案是:通过序列化来实现深拷贝。可以思考一下,为何序列化对象要用深拷贝而不是用浅拷贝呢?
4.2 注意要点
可以序列化是干什么的? 它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。
4.3 序列化案例

看一下下面案例,很简单,只需要实现 Serializable 这个接口。Android 中还可以实现 Parcelable 接口。
public class ColoredCircle implements Serializable {

private int x;
private int y;

public ColoredCircle(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

@Override
public String toString() {
return “x=” + x + “, y=” + y;
}
}
private void test3() {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// 创建原始的可序列化对象
DouBi c1 = new DouBi(100, 100);
System.out.println(“ 原始的对象 = ” + c1);
DouBi c2 = null;
// 通过序列化实现深拷贝
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// 序列化以及传递这个对象
oos.writeObject(c1);
oos.flush();
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bin);
// 返回新的对象
c2 = (DouBi) ois.readObject();
// 校验内容是否相同
System.out.println(“ 复制后的对象 = ” + c2);
// 改变原始对象的内容
c1.setX(200);
c1.setY(200);
// 查看每一个现在的内容
System.out.println(“ 查看原始的对象 = ” + c1);
System.out.println(“ 查看复制的对象 = ” + c2);
} catch (IOException e) {
System.out.println(“Exception in main = ” + e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

输出结果如下:
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始的对象 = x=100, y=100
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 复制后的对象 = x=100, y=100
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 查看原始的对象 = x=200, y=200
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 查看复制的对象 = x=100, y=100

注意:需要做以下几件事儿:

确保对象图中的所有类都是可序列化的
创建输入输出流
使用这个输入输出流来创建对象输入和对象输出流
将你想要拷贝的对象传递给对象输出流
从对象输入流中读取新的对象并且转换回你所发送的对象的类

得出的结论

在这个例子中,创建了一个 DouBi 对象 c1 然后将它序列化 (将它写到 ByteArrayOutputStream 中). 然后我反序列化这个序列化后的对象并将它保存到 c2 中。随后我修改了原始对象 c1。然后结果如你所见,c1 不同于 c2,对 c1 所做的任何修改都不会影响 c2。
注意,序列化这种方式有其自身的限制和问题:因为无法序列化 transient 变量, 使用这种方法将无法拷贝 transient 变量。再就是性能问题。创建一个 socket, 序列化一个对象, 通过 socket 传输它, 然后反序列化它,这个过程与调用已有对象的方法相比是很慢的。所以在性能上会有天壤之别。如果性能对你的代码来说是至关重要的,建议不要使用这种方式。它比通过实现 Clonable 接口这种方式来进行深拷贝几乎多花 100 倍的时间。

05. 延迟拷贝

延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。这个以前几乎都没听说过,后来看书才知道有这么一种拷贝!
当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且, 在某些情况下, 循环引用会导致一些问题。

06. 如何选择拷贝方式

如果对象的属性全是基本类型的,那么可以使用浅拷贝。
如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。
意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。

07. 数组的拷贝
数组除了默认实现了 clone()方法之外,还提供了 Arrays.copyOf 方法用于拷贝,这两者都是浅拷贝。
7.1 基本数据类型数组

如下所示
public void test4() {
int[] lNumbers1 = new int[5];
int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length);
rNumbers1[0] = 1;
boolean first = lNumbers1[0] == rNumbers1[0];
Log.d(“ 小杨逗比 ”, “lNumbers2[0]=” + lNumbers1[0] + “,rNumbers2[0]=” + rNumbers1[0]+”—“+first);

int[] lNumbers3 = new int[5];
int[] rNumbers3 = lNumbers3.clone();
rNumbers3[0] = 1;
boolean second = lNumbers3[0] == rNumbers3[0];
Log.d(“ 小杨逗比 ”, “lNumbers3[0]=” + lNumbers3[0] + “,rNumbers3[0]=” + rNumbers3[0]+”—“+second);
}

打印结果如下所示
2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/ 小杨逗比: lNumbers2[0]=0,rNumbers2[0]=1—false
2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/ 小杨逗比: lNumbers3[0]=0,rNumbers3[0]=1—false

7.2 引用数据类型数组

如下所示
public static void test5() {
People[] lNumbers1 = new People[5];
lNumbers1[0] = new People();
People[] rNumbers1 = lNumbers1;
boolean first = lNumbers1[0].equals(rNumbers1[0]);
Log.d(“ 小杨逗比 ”, “lNumbers1[0]=” + lNumbers1[0] + “,rNumbers1[0]=” + rNumbers1[0]+”–“+first);

People[] lNumbers2 = new People[5];
lNumbers2[0] = new People();
People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
boolean second = lNumbers2[0].equals(rNumbers2[0]);
Log.d(“ 小杨逗比 ”, “lNumbers2[0]=” + lNumbers2[0] + “,rNumbers2[0]=” + rNumbers2[0]+”–“+second);

People[] lNumbers3 = new People[5];
lNumbers3[0] = new People();
People[] rNumbers3 = lNumbers3.clone();
boolean third = lNumbers3[0].equals(rNumbers3[0]);
Log.d(“ 小杨逗比 ”, “lNumbers3[0]=” + lNumbers3[0] + “,rNumbers3[0]=” + rNumbers3[0]+”–“+third);
}

public static class People implements Cloneable {

int age;
Holder holder;

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

public static class Holder {
int holderValue;
}
}

打印日志如下
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/ 小杨逗比: lNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18,rNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18–true
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/ 小杨逗比: lNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671,rNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671–true
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/ 小杨逗比: lNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56,rNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56–true

08. 集合的拷贝
集合的拷贝也是我们平时经常会遇到的,一般情况下,我们都是用浅拷贝来实现,即通过构造函数或者 clone 方法。
8.1 集合浅拷贝

构造函数和 clone() 默认都是浅拷贝
public static void test6() {
ArrayList<People> lPeoples = new ArrayList<>();
People people1 = new People();
lPeoples.add(people1);
Log.d(“ 小杨逗比 ”, “lPeoples[0]=” + lPeoples.get(0));
ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone();
Log.d(“ 小杨逗比 ”, “rPeoples[0]=” + rPeoples.get(0));
boolean b = lPeoples.get(0).equals(rPeoples.get(0));
Log.d(“ 小杨逗比 ”, “ 比较两个对象 ” + b);
}

public static class People implements Cloneable {

int age;
Holder holder;

@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

public static class Holder implements Cloneable {

int holderValue;

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}

打印日志
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/ 小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/ 小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/ 小杨逗比: 比较两个对象 true

8.2 集合深拷贝

在某些特殊情况下,如果需要实现集合的深拷贝,那就要创建一个新的集合,然后通过深拷贝原先集合中的每个元素,将这些元素加入到新的集合当中。
public static void test7() {
ArrayList<People> lPeoples = new ArrayList<>();
People people1 = new People();
people1.holder = new People.Holder();
lPeoples.add(people1);
Log.d(“ 小杨逗比 ”, “lPeoples[0]=” + lPeoples.get(0));
ArrayList<People> rPeoples = new ArrayList<>();
for (People people : lPeoples) {
rPeoples.add((People) people.clone());
}
Log.d(“ 小杨逗比 ”, “rPeoples[0]=” + rPeoples.get(0));
boolean b = lPeoples.get(0).equals(rPeoples.get(0));
Log.d(“ 小杨逗比 ”, “ 比较两个对象 ” + b);
}

public static class People implements Cloneable {

int age;
Holder holder;

@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

public static class Holder implements Cloneable {

int holderValue;

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}

打印日志
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/ 小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/ 小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/ 小杨逗比: 比较两个对象 false

其他介绍
01. 关于博客汇总链接

1. 技术博客汇总

2. 开源项目汇总

3. 生活博客汇总

4. 喜马拉雅音频汇总

5. 其他汇总

02. 关于我的博客

我的个人站点:www.yczbj.org,www.ycbjie.cn
github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/…

简书:http://www.jianshu.com/u/b7b2…

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo…

开源中国:https://my.oschina.net/zbj161…

泡在网上的日子:http://www.jcodecraeer.com/me…

邮箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
segmentfault 头条:https://segmentfault.com/u/xi…

掘金:https://juejin.im/user/593943…

正文完
 0