Java 语言的一个优点就是取消了指针的概念,但也导致了许多程序员在编程中常常忽略了对象与引用的区别,本文会试图澄清这一概念。并且由于Java不能通过简单 的赋值来解决对象复制的问题,在开发过程中,也常常要要应用clone()方法来复制对象。本文会让你了解什么是影子clone与深度clone,认识它 们的区别、优点及缺点。

看到这个标题,是不是有点困惑:Java
言明确说明取消了指针,因为指针往往是在带来方便的同时也是导致代码不安全的根源,同时也会使程序的变得非常复杂难以理解,滥用指针写成的代码不亚于使用
早已臭名昭著的"GOTO"语句。Java放弃指针的概念绝对是极其明智的。但这只是在Java语言中没有明确的指针定义,实质上每一个new语句返回的
都是一个指针的引用,只不过在大多时候Java中不用关心如何操作这个"指针",更不用象在操作C++的指针那样胆战心惊。唯一要多多关心的是在给函数传
递对象的时候。

  1. package com.zoer.src;
  2. public class ObjRef {
  3. Obj aObj = new Obj();
  4. int aInt = 11;
  5. public void changeObj(Obj inObj) {
  6. inObj.str = "changed value";
  7. }
  8. public void changePri(int inInt) {
  9. inInt = 22;
  10. }
  11. public static void main(String[] args) {
  12. ObjRef oRef = new ObjRef();
  13. System.out.println("Before call changeObj() method: " + oRef.aObj);
  14. oRef.changeObj(oRef.aObj);
  15. System.out.println("After call changeObj() method: " + oRef.aObj);
  16. System.out.println("==================Print Primtive=================");
  17. System.out.println("Before call changePri() method: " + oRef.aInt);
  18. oRef.changePri(oRef.aInt);
  19. System.out.println("After call changePri() method: " + oRef.aInt);
  20. }
  21. }
  1. package com.zoer.src;
  2. public class Obj {
  3. String str = "init value";
  4. public String toString() {
  5. return str;
  6. }
  7. }

这段代码的主要部分调用了两个很相近的方法,changeObj()和changePri()。唯一不同的是它们一个把对象作为输入参数,另一个把
Java中的基本类型int作为输入参数。并且在这两个函数体内部都对输入的参数进行了改动。看似一样的方法,程序输出的结果却不太一样。
changeObj()方法真正的把输入的参数改变了,而changePri()方法对输入的参数没有任何的改变。

从这个例子知道Java对对象和基本的数据类型的处理是不一样的。和C语言一样,当把Java的基本数据类型(如int,char,double等)作为入口参数传给函数体的时候,传入的参数在函数体内部变成了局部变量,这个局部变量是输入参数的一个拷贝,所有的函数体内部的操作都是针对这个拷贝的操作,函数执行结束后,这个局部变量也就完成了它的使命,它影响不到作为输入参数的变量。这种方式的参数传递被称为"值传递"。而在Java中用对象作为入口参数的传递则缺省为"引用传递"也就是说仅仅传递了对象的一个"引用"这个"引用"的概念同C语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个对象的直接操作。 

除了在函数传值的时候是"引用传递",在任何用"="向对象变量赋值的时候都是"引用传递"。就是类似于给变量再起一个别名。两个名字都指向内存中的同一个对象。
      在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
     
Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说
明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用new操作符
返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。
      怎样应用clone()方法?

一个很典型的调用clone()代码如下:

  1. public class CloneClass implements Cloneable {
  2. public int aInt;
  3. public Object clone() {
  4. CloneClass o = null;
  5. try {
  6. o = (CloneClass) super.clone();
  7. } catch (CloneNotSupportedException e) {
  8. e.printStackTrace();
  9. }
  10. return o;
  11. }
  12. }

有三个值得注意的地方,一是希望能实现clone功能的CloneClass类实现了Cloneable接口,这个接口属于java.lang
包,java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。另一个值得请注意的是重载了clone()方
法。最后在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或间接调用了java.lang.Object类的clone()方法。下面再详细的解释一下这几点。

应该说第三点是最重要的,仔细观察一下Object类的clone()一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能。对于第二点,也要观察Object类中的clone()还是一个protected
性的方法。这也意味着如果要应用clone()方法,必须继承Object类,在Java中所有的类是缺省继承Object类的,也就不用关心这点了。然
后重载clone()方法。还有一点要考虑的是为了让其它类能调用这个clone类的clone()方法,重载之后要把clone()方法的属性设置为public
     
 那么clone类为什么还要实现Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而
且这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()
方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出
CloneNotSupportedException异常。 
      以上是clone的最基本的步骤,想要完成一个成功的clone,还要了解什么是"影子clone"和"深度clone"。 
      什么是影子clone?

  1. package com.zoer.src;
  2. class UnCloneA {
  3. private int i;
  4. public UnCloneA(int ii) {
  5. i = ii;
  6. }
  7. public void doublevalue() {
  8. i *= 2;
  9. }
  10. public String toString() {
  11. return Integer.toString(i);
  12. }
  13. }
  14. class CloneB implements Cloneable {
  15. public int aInt;
  16. public UnCloneA unCA = new UnCloneA(111);
  17. public Object clone() {
  18. CloneB o = null;
  19. try {
  20. o = (CloneB) super.clone();
  21. } catch (CloneNotSupportedException e) {
  22. e.printStackTrace();
  23. }
  24. return o;
  25. }
  26. }
  27. public class ObjRef {
  28. public static void main(String[] a) {
  29. CloneB b1 = new CloneB();
  30. b1.aInt = 11;
  31. System.out.println("before clone,b1.aInt = " + b1.aInt);
  32. System.out.println("before clone,b1.unCA = " + b1.unCA);
  33. CloneB b2 = (CloneB) b1.clone();
  34. b2.aInt = 22;
  35. b2.unCA.doublevalue();
  36. System.out.println("=================================");
  37. System.out.println("after clone,b1.aInt = " + b1.aInt);
  38. System.out.println("after clone,b1.unCA = " + b1.unCA);
  39. System.out.println("=================================");
  40. System.out.println("after clone,b2.aInt = " + b2.aInt);
  41. System.out.println("after clone,b2.unCA = " + b2.unCA);
  42. }
  43. }

输出结果:

before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 222
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222

输出的结果说明int类型的变量aInt和UnCloneA的实例对象unCA的clone结果不一致,int类型是真正的被clone了,因为改变了
b2中的aInt变量,对b1的aInt没有产生影响,也就是说,b2.aInt与b1.aInt已经占据了不同的内存空间,b2.aInt是
b1.aInt的一个真正拷贝。相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCA和b1.unCA是仅仅指向同一个对象的不同引用!从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。 

大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为"影子clone"。要想让b2.unCA指向与b2.unCA不同的对象,而且b2.unCA中还要包含b1.unCA中的信息作为初始信息,就要实现深度clone。
       怎么进行深度clone? 
     
 把上面的例子改成深度clone很简单,需要两个改变:一是让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable
接口,重载clone()方法)。二是在CloneB的clone()方法中加入一句
o.unCA = (UnCloneA)unCA.clone();

  1. package com.zoer.src;
  2. class UnCloneA implements Cloneable {
  3. private int i;
  4. public UnCloneA(int ii) {
  5. i = ii;
  6. }
  7. public void doublevalue() {
  8. i *= 2;
  9. }
  10. public String toString() {
  11. return Integer.toString(i);
  12. }
  13. public Object clone() {
  14. UnCloneA o = null;
  15. try {
  16. o = (UnCloneA) super.clone();
  17. } catch (CloneNotSupportedException e) {
  18. e.printStackTrace();
  19. }
  20. return o;
  21. }
  22. }
  23. class CloneB implements Cloneable {
  24. public int aInt;
  25. public UnCloneA unCA = new UnCloneA(111);
  26. public Object clone() {
  27. CloneB o = null;
  28. try {
  29. o = (CloneB) super.clone();
  30. } catch (CloneNotSupportedException e) {
  31. e.printStackTrace();
  32. }
  33. o.unCA = (UnCloneA) unCA.clone();
  34. return o;
  35. }
  36. }
  37. public class CloneMain {
  38. public static void main(String[] a) {
  39. CloneB b1 = new CloneB();
  40. b1.aInt = 11;
  41. System.out.println("before clone,b1.aInt = " + b1.aInt);
  42. System.out.println("before clone,b1.unCA = " + b1.unCA);
  43. CloneB b2 = (CloneB) b1.clone();
  44. b2.aInt = 22;
  45. b2.unCA.doublevalue();
  46. System.out.println("=================================");
  47. System.out.println("after clone,b1.aInt = " + b1.aInt);
  48. System.out.println("after clone,b1.unCA = " + b1.unCA);
  49. System.out.println("=================================");
  50. System.out.println("after clone,b2.aInt = " + b2.aInt);
  51. System.out.println("after clone,b2.unCA = " + b2.unCA);
  52. }
  53. }

输出结果:

before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 111
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222

可以看出,现在b2.unCA的改变对b1.unCA没有产生影响。此时b1.unCA与b2.unCA指向了两个不同的UnCloneA实例,而且在
CloneB b2 = (CloneB)b1.clone();调用的那一刻b1和b2拥有相同的值,在这里,b1.i = b2.i = 11。

要知道不是所有的类都能实现深度clone的。例如,如果把上面的CloneB类中的UnCloneA类型变量改成StringBuffer类型,看一下JDK API中关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer还是一个final类
这也是说我们也不能用继承的办法间接实现StringBuffer的clone。如果一个类中包含有StringBuffer类型对象或和
StringBuffer相似类的对象,我们有两种选择:要么只能实现影子clone,要么就在类的clone()方法中加一句(假设是
SringBuffer对象,而且变量名仍是
unCA): o.unCA = new StringBuffer(unCA.toString()); //原来的
是:o.unCA = (UnCloneA)unCA.clone();

还要知道的是除了基本数据类型能自动实现深度clone以外,String对象是一个例外,它clone后的表现好象也实现了深度clone,虽然这只是一个假象,但却大大方便了我们的编程。 
       Clone中String和StringBuffer的区别 
       应该说明的是,这里不是着重说明String和StringBuffer的区别,但从这个例子里也能看出String类的一些与众不同的地方。 
     
 下面的例子中包括两个类,CloneC类包含一个String类型变量和一个StringBuffer类型变量,并且实现了clone()方法。在
StrClone类中声明了CloneC类型变量c1,然后调用c1的clone()方法生成c1的拷贝c2,在对c2中的String和
StringBuffer类型变量用相应的方法改动之后打印结果:

  1. package com.zoer.src;
  2. class CloneC implements Cloneable {
  3. public String str;
  4. public StringBuffer strBuff;
  5. public Object clone() {
  6. CloneC o = null;
  7. try {
  8. o = (CloneC) super.clone();
  9. } catch (CloneNotSupportedException e) {
  10. e.printStackTrace();
  11. }
  12. return o;
  13. }
  14. }
  15. public class StrClone {
  16. public static void main(String[] a) {
  17. CloneC c1 = new CloneC();
  18. c1.str = new String("initializeStr");
  19. c1.strBuff = new StringBuffer("initializeStrBuff");
  20. System.out.println("before clone,c1.str = " + c1.str);
  21. System.out.println("before clone,c1.strBuff = " + c1.strBuff);
  22. CloneC c2 = (CloneC) c1.clone();
  23. c2.str = c2.str.substring(0, 5);
  24. c2.strBuff = c2.strBuff.append(" change strBuff clone");
  25. System.out.println("=================================");
  26. System.out.println("after clone,c1.str = " + c1.str);
  27. System.out.println("after clone,c1.strBuff = " + c1.strBuff);
  28. System.out.println("=================================");
  29. System.out.println("after clone,c2.str = " + c2.str);
  30. System.out.println("after clone,c2.strBuff = " + c2.strBuff);
  31. }
  32. }

执行结果:

  1. <span style="font-family:'Microsoft YaHei';"><span style="font-size:16px;">before clone,c1.str = initializeStr
  2. before clone,c1.strBuff = initializeStrBuff
  3. =================================
  4. after clone,c1.str = initializeStr
  5. after clone,c1.strBuff = initializeStrBuff change strBuff clone
  6. =================================
  7. after clone,c2.str = initi
  8. after clone,c2.strBuff = initializeStrBuff change strBuff clone
  9. </span></span>

打印的结果可以看出,String类型的变量好象已经实现了深度clone,因为对c2.str的改动并没有影响到c1.str!难道Java把
Sring类看成了基本数据类型?其实不然,这里有一个小小的把戏,秘密就在于c2.str = c2.str.substring(0,5)这一语句!
实质上,在clone的时候c1.str与c2.str仍然是引用,而且都指向了同一个String对象。但在执行
c2.str = c2.str.substring(0,5)的时候,它作用相当于生成了一个新的String类型,然后又赋回给c2.str。这是因
为String被Sun公司的工程师写成了一个不可更改的类(immutable class),在所有String类中的函数都不能更改自身的值。

java中的深复制和浅复制的更多相关文章

  1. Java中对象的深复制和浅复制详解

    1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象. ⑵ ...

  2. Java中的深复制与浅复制

    1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅复制仅仅复制所考虑的对象,而不 复制它所引用的对象. ...

  3. Java中的clone()----深复制,浅复制

    这篇文章主要介绍了Java中对象的深复制(深克隆)和浅复制(浅克隆) ,需要的朋友可以参考下 1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他 ...

  4. js中的深复制与浅复制

    前言 所谓深复制与浅复制(深拷贝与浅拷贝),乍一听感觉听高大上,像是一个非常难理解的概念,其实我们平常项目开发都是在用的,只是你可能不知道该怎么叫它的名字而已,就像你听熟了一首歌,就是不知道这首歌叫什 ...

  5. C#中的深复制与浅复制

    C#中分为值类型和引用类型,值类型的变量直接包含其数据,而引用类型的变量则存储对象的引用. 对于值类型,每个变量都有自己的数据副本,对一个变量的操作不可能影响到另一个变量.如 class Progra ...

  6. js中的深复制和浅复制

    在实际情况中经常会遇到对对象复制的问题.比如在处理项目中的一笔多结构的数据存储或者调用,这个时候你就要对对象(json)进行操作,而不同的操作根据不同的需求来定义.其中最常见最普遍的是对对象的复制,重 ...

  7. clone()方法、深复制和浅复制

    clone方法 Java中没有明确提供指针的概念和用法,而实质上没个new语句返回的都是一个指针的引用,只不过在大部分情况下开发人员不需要关心如何去操作这个指针而已. 在实际编程中,经常会遇到从某个已 ...

  8. .Net深复制、浅复制

    在.Net,大家都知道引用类型的深复制.浅复制吧. ,一般int等值类型是值类型(复制时是直接传值),一般的类(List<T>,Class)是引用类型(复制时传地址),默认是浅复制.若ob ...

  9. C++学习基础七——深复制与浅复制

    一.深复制与浅复制基本知识 深复制和浅复制,又称为深拷贝和浅拷贝. 深复制和浅复制的区别如下图1所示: 图1 图1表示的是,定义一个类CDemo,包含int a和char *str两个成员变量, 当深 ...

  10. iOS 集合的深复制与浅复制

    概念 对象拷贝有两种方式:浅复制和深复制.顾名思义,浅复制,并不拷贝对象本身,仅仅是拷贝指向对象的指针:深复制是直接拷贝整个对象内存到另一块内存中. 一图以蔽之 再简单些说:浅复制就是指针拷贝:深复制 ...

随机推荐

  1. SaltStack快速入门-配置管理

    1:定义远程配置时描述位置,salt配置用的是一种yaml的描述语法,saltstack也是可以分环境的,比如测试环境.生产环境,默认是base,base也是必须存在的,修改内容如下: file_ro ...

  2. 【PyTorch深度学习60分钟快速入门 】Part3:神经网络

      神经网络可以通过使用torch.nn包来构建. 既然你已经了解了autograd,而nn依赖于autograd来定义模型并对其求微分.一个nn.Module包含多个网络层,以及一个返回输出的方法f ...

  3. Django之模板层

    在一个项目里面有一个专门放模板的文件夹Templates,有一个专门放视图的文件views,而且我们大多给浏览器响应的都应该是一个完整的页面,也就是读取的是一个HTML文件,然后再返回给浏览器.但我们 ...

  4. ssh采用expect实现自动输入密码登录、拷贝

    1. 引言 最近做了一个项目,需要频繁与另一台主机进行文件的传输:中间想到了很多方式:FTP.samba.curl等,但是还是感觉scp最好用. SCP使用教程可参阅:http://www.jb51. ...

  5. C# Code First 配置(二)

    上一篇文章地址 C# Code First 配置 此文章主要介绍配置映射到表中的详细字段.信息等,如下: System.ComponentModel.DataAnnotations 包含的特性: At ...

  6. [转]ui-grid User can't select the row by clicking the select checkbox available in the respective row when enableFullRowSelection : true"

    本文转自:https://github.com/angular-ui/ui-grid/issues/5239 Try this style to enable checkbox selection: ...

  7. 一个比较好用的省内存的ORM

    http://www.52chloe.com 记录一下,完了,就这样

  8. Java语言的简介

    Java语言的由来 Java是由Sun Microsystems公司推出的Java面向对象程序设计语言(以下简称Java语言)和Java平台的总称.由James Gosling和同事们共同研发,并在1 ...

  9. 【Mysql】mysql乐观锁总结和实践

    乐观锁介绍: 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突 ...

  10. eclipse使用struts2找不到action方法或找不到action的错误记录

    在确认web.xml已经配置, 配置好struts.xml , 代码没有报错, jar包没有问题, 服务器也没有问题, 代码逻辑没有问题, 关键字方法名action都没有写错, 可以运行旧的相同的代码 ...