热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java浅拷贝和深拷贝那些事

假如说你想复制一个简单变量。很简单:intapples5;intpearsapples;不仅仅是int类型,其它七种原始数据类型(boolean,ch

假如说你想复制一个简单变量。很简单:

int apples = 5;int pears = apples;

不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。

但是如果你复制的是一个对象,情况就有些复杂了。假设说我是一个beginner,我会这样写:

class Student {private int number;public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}}
public class Test {public static void main(String args[]) {Student stu1 = new Student();stu1.setNumber(12345);Student stu2 = stu1;System.out.println("学生1:" + stu1.getNumber());System.out.println("学生2:" + stu2.getNumber());}
}


打印结果:

学生1:12345
学生2:12345


这里我们自定义了一个学生类,该类只有一个number字段。

我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)

再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

难道真的是这样吗?我们试着改变stu2实例的number字段,再打印结果看看:

stu2.setNumber(54321);System.out.println("学生1:" + stu1.getNumber());System.out.println("学生2:" + stu2.getNumber());

打印结果:

学生1:54321
学生2:54321



这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢?

原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,

这样,stu1和stu2指向内存堆中同一个对象。如图:



那么,怎样才能达到复制一个对象呢?

是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。

该方法的签名是:protected native Object clone() throws CloneNotSupportedException;

因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。

要想对一个对象进行复制,就需要对clone方法覆盖。


一般步骤是(浅复制):

1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)

2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象(native为本地方法)。

我们先看看Object的clone()源代码是咋个说的。

Object的clone()源代码简介

/*** Creates and returns a copy of this {@code Object}. The default* implementation returns a so-called "shallow" copy: It creates a new* instance of the same class and then copies the field values (including* object references) from this instance to the new instance. A "deep" copy,* in contrast, would also recursively clone nested objects. A subclass that* needs to implement this kind of cloning should call {@code super.clone()}* to create the new instance and then create deep copies of the nested,* mutable objects.** @return a copy of this object.* @throws CloneNotSupportedException* if this object's class does not implement the {@code* Cloneable} interface.*/protected Object clone() throws CloneNotSupportedException {if (!(this instanceof Cloneable)) {throw new CloneNotSupportedException("Class doesn't implement Cloneable");}return internalClone((Cloneable) this);}/** Native helper method for cloning.*/private native Object internalClone(Cloneable o);



从上面的介绍看出, clone方法首先会判对象是否实现了Cloneable接口,若无则抛出CloneNotSupportedException, 最后会调用internalClone. intervalClone是一个native方法,一般来说native方法的执行效率高于非native方法。
当某个类要复写clone方法时,要继承Cloneable接口。通常的克隆对象都是通过super.clone()方法来克隆对象。正如这个介绍所说的一样,如果要deep copy,则要递归低调用每个嵌套的属性对象的clone()方法。

下面对上面那个方法进行改造:

class Student implements Cloneable{private int number;public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}@Overridepublic Object clone() {Student stu = null;try{stu = (Student)super.clone();}catch(CloneNotSupportedException e) {e.printStackTrace();}return stu;}
}

public class Test {public static void main(String args[]) {Student stu1 = new Student();stu1.setNumber(12345);Student stu2 = (Student)stu1.clone();System.out.println("学生1:" + stu1.getNumber());System.out.println("学生2:" + stu2.getNumber());stu2.setNumber(54321);System.out.println("学生1:" + stu1.getNumber());System.out.println("学生2:" + stu2.getNumber());}
}


打印结果:

学生1:12345
学生2:12345
学生1:12345
学生2:54321



如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

System.out.println(stu1 == stu2); // false

上面的复制被称为浅复制(Shallow Copy,因为它对嵌套的属性对象也是直接把另外一个对象中的嵌套属性对象的引用拿过来赋值)。

我们在学生类里再加一个Address类。

class Address {private String add;public String getAdd() {return add;}public void setAdd(String add) {this.add = add;}}class Student implements Cloneable{private int number;private Address addr;public Address getAddr() {return addr;}public void setAddr(Address addr) {this.addr = addr;}public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}@Overridepublic Object clone() {Student stu = null;try{stu = (Student)super.clone();}catch(CloneNotSupportedException e) {e.printStackTrace();}return stu;}
}
public class Test {public static void main(String args[]) {Address addr = new Address();addr.setAdd("杭州市");Student stu1 = new Student();stu1.setNumber(123);stu1.setAddr(addr);Student stu2 = (Student)stu1.clone();System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());}
}



打印结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市



乍一看没什么问题,真的是这样吗?

我们在main方法中试着改变addr实例的地址。

addr.setAdd("西湖区");System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());

打印结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:西湖区



这就奇怪了,怎么两个学生的地址都改变了?原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要深度复制(deep copy)。

我们需要将Address类可复制化,并且修改clone方法,完整代码如下:

package abc;class Address implements Cloneable {private String add;public String getAdd() {return add;}public void setAdd(String add) {this.add = add;}@Overridepublic Object clone() {Address addr = null;try{addr = (Address)super.clone();}catch(CloneNotSupportedException e) {e.printStackTrace();}return addr;}
}class Student implements Cloneable{private int number;private Address addr;public Address getAddr() {return addr;}public void setAddr(Address addr) {this.addr = addr;}public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}@Overridepublic Object clone() {Student stu = null;try{stu = (Student)super.clone(); //浅复制}catch(CloneNotSupportedException e) {e.printStackTrace();}stu.addr = (Address)addr.clone(); //深度复制return stu;}
}
public class Test {public static void main(String args[]) {Address addr = new Address();addr.setAdd("杭州市");Student stu1 = new Student();stu1.setNumber(123);stu1.setAddr(addr);Student stu2 = (Student)stu1.clone();System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());addr.setAdd("西湖区");System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());}
}


打印结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:杭州市



这样结果就符合我们的想法了。最后我们可以看看API里其中一个实现了clone方法的类:

java.util.Date:

/*** Return a copy of this object.*/public Object clone() {Date d = null;try {d = (Date)super.clone();if (cdate != null) {d.cdate = (BaseCalendar.Date) cdate.clone();}} catch (CloneNotSupportedException e) {} // Won't happenreturn d;}

类其实也属于深度复制。


推荐阅读
  • Objective-C 中的委托模式详解与应用 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • 属性类 `Properties` 是 `Hashtable` 类的子类,用于存储键值对形式的数据。该类在 Java 中广泛应用于配置文件的读取与写入,支持字符串类型的键和值。通过 `Properties` 类,开发者可以方便地进行配置信息的管理,确保应用程序的灵活性和可维护性。此外,`Properties` 类还提供了加载和保存属性文件的方法,使其在实际开发中具有较高的实用价值。 ... [详细]
  • 在前文探讨了Spring如何为特定的bean选择合适的通知器后,本文将进一步深入分析Spring AOP框架中代理对象的生成机制。具体而言,我们将详细解析如何通过代理技术将通知器(Advisor)中包含的通知(Advice)应用到目标bean上,以实现切面编程的核心功能。 ... [详细]
  • 大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式
    大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式 ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 在尝试对 QQmlPropertyMap 类进行测试驱动开发时,发现其派生类中无法正常调用槽函数或 Q_INVOKABLE 方法。这可能是由于 QQmlPropertyMap 的内部实现机制导致的,需要进一步研究以找到解决方案。 ... [详细]
  • Android 构建基础流程详解
    Android 构建基础流程详解 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 本文深入解析了WCF Binding模型中的绑定元素,详细介绍了信道、信道管理器、信道监听器和信道工厂的概念与作用。从对象创建的角度来看,信道管理器负责信道的生成。具体而言,客户端的信道通过信道工厂进行实例化,而服务端则通过信道监听器来接收请求。文章还探讨了这些组件之间的交互机制及其在WCF通信中的重要性。 ... [详细]
  • 在使用 Qt 进行 YUV420 图像渲染时,由于 Qt 本身不支持直接绘制 YUV 数据,因此需要借助 QOpenGLWidget 和 OpenGL 技术来实现。通过继承 QOpenGLWidget 类并重写其绘图方法,可以利用 GPU 的高效渲染能力,实现高质量的 YUV420 图像显示。此外,这种方法还能显著提高图像处理的性能和流畅性。 ... [详细]
  • AIX编程挑战赛:AIX正方形问题的算法解析与Java代码实现
    在昨晚的阅读中,我注意到了CSDN博主西部阿呆-小草屋发表的一篇文章《AIX程序设计大赛——AIX正方形问题》。该文详细阐述了AIX正方形问题的背景,并提供了一种基于Java语言的解决方案。本文将深入解析这一算法的核心思想,并展示具体的Java代码实现,旨在为参赛者和编程爱好者提供有价值的参考。 ... [详细]
  • JavaScript XML操作实用工具类:XmlUtilsJS技巧与应用 ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
  • 数组容量的动态调整与优化策略
    在探讨数组容量动态调整与优化策略时,本文分析了两种常见的方法。首先,通过使用for循环逐个复制元素实现扩容,但这种方法存在计算索引的复杂性问题。其次,利用System.arraycopy()方法进行高效复制,显著提升了性能和代码可读性。此外,文章还讨论了动态数组在不同应用场景下的优化策略,包括预分配容量和按需扩展等技术,以提高程序的整体效率。 ... [详细]
author-avatar
手机用户2502895293
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有