深入理解java中的clone.docx
- 文档编号:9652313
- 上传时间:2023-02-05
- 格式:DOCX
- 页数:16
- 大小:72.10KB
深入理解java中的clone.docx
《深入理解java中的clone.docx》由会员分享,可在线阅读,更多相关《深入理解java中的clone.docx(16页珍藏版)》请在冰豆网上搜索。
深入理解java中的clone
深入理解java中的clone
如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。
预备知识
为了理解java的clone,有必要先温习以下的知识。
java的类型,java的类型分为两大类,一类为primitive,如int,另一类为引用类型,如String,Object等等。
java引用类型的存储,java的引用类型都是存储在堆上的。
publicclassB{
inta;
Stringb;
publicB(inta,Stringb){
super();
this.a=a;
this.b=b;
}
}
对这样一个引用类型的实例,我们可以推测,在堆上它的内存存储形式(除去指向class的引用,锁的管理等等内务事务所占内存),应该有一个int值表示a,以及一个引用,该引用指向b在堆上的存储空间。
为什么要clone
恩,因为需要。
废话。
有名的GoF设计模式里有一个模式为原型模式,用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.
简单的说就是clone一个对象实例。
使得clone出来的copy和原有的对象一模一样。
插一个简单使用clone的例子,如果一个对象内部有可变对象实例的话,publicAPI不应该直接返回该对象的引用,以防调用方的code改变该对象的内部状态。
这个时候可以返回该对象的clone。
问题来了,什么叫一模一样。
一般来说,有
x.clone()!
=x (存储的位置不一样)
x.clone().getClass()==x.getClass() (类型是一样的)
x.clone().equals(x) (内容是一样的)
但是这些都不是强制的。
我们需要什么样的clone就搞出什么样的clone好了。
一般而言,我们要的clone应该是这样的。
copy和原型的内容一样,但是又是彼此隔离的。
即在clone之后,改变其中一个不影响另外一个。
Object的clone以及为什么如此实现
Object的clone的行为是最简单的。
以堆上的内存存储解释的话(不计内务内存),对一个对象a的clone就是在堆上分配一个和a在堆上所占存储空间一样大的一块地方,然后把a的堆上内存的内容复制到这个新分配的内存空间上。
看例子。
packagec.d.e;
classUser{
Stringname;
intage;
}
classAccountimplementsCloneable{
Useruser;
longbalance;
@Override
publicObjectclone()throwsCloneNotSupportedException{
returnsuper.clone();
}
}
publicclassTest{
publicstaticvoidmain(String[]args)throwsCloneNotSupportedException{
//user.
Useruser=newUser();
user.name="user";
user.age=20;
//account.
Accountaccount=newAccount();
account.user=user;
account.balance=10000;
//copy.
Accountcopy=(Account)account.clone();
//balance因为是primitive,所以copy和原型是相等且独立的。
System.out.println(copy.balance==account.balance);
copy.balance=20000;
//改变copy不影响原型。
System.out.println(copy.balance!
=account.balance);
//user因为是引用类型,所以copy和原型的引用是同一的。
System.out.println((copy.user==account.user));
copy.user.name="newName";
//改变的是同一个东西。
System.out.println(account.user.name);
}
}
恩,默认实现是帮了我们一些忙,但是不是全部。
primitive的确做到了相等且隔离。
引用类型仅仅是复制了一下引用,copy和原型引用的东西是一样的。
这个就是所谓的浅copy了。
要实现深copy,即复制原型中对象的内存copy,而不仅仅是一个引用。
只有自己动手了。
等等,是不是所有的引用类型都需要深copy呢?
不是!
我们之所以要深copy,是因为默认的实现提供的浅copy不是隔离的,换言之,改变copy的东西,会影响到原型的内部。
比如例子中,改变copy的user的name,影响了原型。
如果我们要copy的类是不可变的呢,如String,没有方法可以改变它的内部状态呢。
packagec.d.e;
classUser{
Stringname;
intage;
}
classAccountimplementsCloneable{
Useruser;
longbalance;
Stringstr1;
@Override
publicObjectclone()throwsCloneNotSupportedException{
returnsuper.clone();
}
}
publicclassTest{
publicstaticvoidmain(String[]args)throwsCloneNotSupportedException{
//user.
Useruser=newUser();
user.name="user";
user.age=20;
//account.
Accountaccount=newAccount();
account.user=user;
account.balance=10000;
account.str1="ljh";
//copy.
Accountcopy=(Account)account.clone();
//balance因为是primitive,所以copy和原型是相等且独立的。
System.out.println(copy.balance==account.balance);
copy.balance=20000;
//改变copy不影响原型。
System.out.println(copy.balance!
=account.balance);
//user因为是引用类型,所以copy和原型的引用是同一的。
System.out.println((copy.user==account.user));
System.out.println(copy.str1);
copy.user.name="newName";
//改变的是同一个东西。
System.out.println(account.user.name);
//String为不可变类。
没有办法可以通过对copy.name的字符串的操作改变这个字符串。
//改变引用新的对象不会影响原型。
copy.str1="cmx";
System.out.println(copy.str1);
System.out.println(account.str1);
//所有的数组都被视为实现接口Cloneable
String[]str3=newString[]{"aaa","bbb","ccc"};
String[]str4=str3.clone();
for(inti=0;i System.out.println(str4[i]); } } 可见,在考虑clone时,primitive和不可变对象类型是可以同等对待的。 java为什么如此实现clone呢? 也许有以下考虑。 1效率和简单性,简单的copy一个对象在堆上的的内存比遍历一个对象网然后内存深copy明显效率高并且简单。 2不给别的类强加意义。 如果A实现了Cloneable,同时有一个引用指向B,如果直接复制内存进行深copy的话,意味着B在意义上也是支持Clone的,但是这个是在使用B的A中做的,B甚至都不知道。 破坏了B原有的接口。 3有可能破坏语义。 如果A实现了Cloneable,同时有一个引用指向B,该B实现为单例模式,如果直接复制内存进行深copy的话,破坏了B的单例模式。 4方便且更灵活,如果A引用一个不可变对象,则内存deepcopy是一种浪费。 Shadowcopy给了程序员更好的灵活性。 clone()对Object类对象本身是不可见的。 所以你会发现找不到clone()方法 原因是: clone()方法是protected访问权限 如何clone clone三部曲。 1声明实现Cloneable接口。 2调用super.clone拿到一个对象,如果父类的clone实现没有问题的话,在该对象的内存存储中,所有父类定义的field都已经clone好了,该类中的primitive和不可变类型引用也克隆好了,可变类型引用都是浅copy。 3把浅copy的引用指向原型对象新的克隆体。 给个例子。 classUserimplementsCloneable{ Stringname; intage; @Override publicUserclone()throwsCloneNotSupportedException{ return(User)super.clone(); } } classAccountimplementsCloneable{ Useruser; longbalance; @Override publicAccountclone()throwsCloneNotSupportedException{ Accountaccount=null; account=(Account)super.clone(); if(user! =null){ account.user=user.clone(); } returnaccount; } } 对clone的态度 clone嘛,我觉得是个好东西,毕竟系统默认实现已经帮我们做了很多事情了。 但是它也是有缺点的。 1手工维护clone的调用链。 这个问题不大,程序员有责任做好。 2如果class的field是个final的可变类,就不行了。 三部曲的第三步没有办法做了。 考虑一个类对clone的态度,有如下几种。 1公开支持: 好吧,按照clone三部曲实现吧。 前提是父类支持(公开或者默默)。 2默默支持: 不实现Cloneable接口,但是在类里面有正确的protected的clone实现,这样,该类不支持clone,但是它的子类如果想支持的话也不妨碍。 3不支持: 好吧,为了明确该目的,提供一个抛CloneNotSupportedException异常的protected的clone实现。 4看情况支持: 该类内部可以保存其他类的实例,如果其他类支持则该类支持,如果其他类不支持,该类没有办法,只有不支持。 其他的选择 可以用原型构造函数,或者静态copy方法来手工制作一个对象的copy。 好处是即使class的field为final,也不会影响该方法的使用。 不好的地方是所有的primitive赋值都得自己维护。 和Serializable的比较 使用Serializable同样可以做到对象的clone。 但是: Cloneable本身就是为clone设计的,虽然有一些缺点,但是如果它可以clone的话无疑用它来做clone比较合适。 如果不行的话用原型构造函数,或者静态copy方法也可以。 Serializable制作clone的话,添加了太多其它的东西,增加了复杂性。 1所有的相关的类都得支持Serializable。 这个相比支持Cloneable只会工作量更大 2Serializable添加了更多的意义,除了提供一个方法用Serializable制作Clone,该类等于也添加了其它的publicAPI,如果一个类实现了Serializable,等于它的2进制形式就已经是其API的一部分了,不便于该类以后内部的改动。 3当类用Serializable来实现clone时,用户如果保存了一个老版本的对象2进制,该类升级,用户用新版本的类反系列化该对象,再调用该对象用Serializable实现的clone。 这里为了一个clone的方法又引入了类版本兼容性的问题。 不划算。 性能 不可否认,JVM越来越快了。 但是系统默认的native实现还是挺快的。 clone一个有100个元素的int数组,用系统默认的clone比静态copy方法快2倍左右。 Clone中String和StringBuffer的区别 应该说明的是,这里不是着重说明String和StringBuffer的区别,但从这个例子里也能看出String类的一些与众不同的地方。 下面的例子中包括两个类,CloneC类包含一个String类型变量和一个StringBuffer类型变量,并且实现了clone()方法。 在 StrClone类中声明了CloneC类型变量c1,然后调用c1的clone()方法生成c1的拷贝c2,在对c2中的String和 StringBuffer类型变量用相应的方法改动之后打印结果。 ljh: String是不可变类,因此克隆出来的对象是不可变的,如果一个对象A中包含了String对象str1成员,和一个StringBuffer对象成员strbf1,然后A通过其克隆方法得到了B,B中对str1的修改不会影响到A中str1成员,因为是String对象是不可变的,所以克隆对象中String对象成员的变化和原来对象的String对象成员的变化是完全独立的。 而StringBuffer对象是可变的,因此克隆出来的B中的StringBuff成员的变化会影响到原始对象A中StringBuffer对象成员的变化。 不可变类成员以及primitive成员的在克隆出来后和原来的母对象中对应的成员是完全独立的。 packagec.d.e; classCloneCimplementsCloneable{ publicStringstr; publicStringBufferstrBuff; publicObjectclone(){ CloneCo=null; try{ o=(CloneC)super.clone(); }catch(CloneNotSupportedExceptione){ e.printStackTrace(); } returno; } } publicclassStrClone{ publicstaticvoidmain(String[]a){ CloneCc1=newCloneC(); c1.str=newString("initializeStr"); c1.strBuff=newStringBuffer("initializeStrBuff"); System.out.println("beforeclone,c1.str="+c1.str); System.out.println("beforeclone,c1.strBuff="+c1.strBuff); CloneCc2=(CloneC)c1.clone(); c2.str=c2.str.substring(0,5); c2.strBuff=c2.strBuff.append("changestrBuffclone"); System.out.println("================================="); System.out.println("afterclone,c1.str="+c1.str); System.out.println("afterclone,c1.strBuff="+c1.strBuff); System.out.println("================================="); System.out.println("afterclone,c2.str="+c2.str); System.out.println("afterclone,c2.strBuff="+c2.strBuff); } } 打印的结果可以看出,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公司的工程师写成了一个不可更改的类(immutableclass),在所有String类中的函数都不能更改自身的值。 应该知道的是在Java中所有的基本数据类型都有一个相对应的类,象Integer类对应int类型,Double类对应double类型等等,这些类也与String类相同,都是不可以改变的类。 也就是说,这些的类中的所有方法都是不能改变其自身的值的。 这也让我们在编clone类的时候有了一个更多的 选择。 同时我们也可以把自己的类编成不可更改的类。 附录: javaapi文档说明 java.lang 接口Cloneable 所有已知子接口: AclEntry,Attribute,AttributedCharacterIterator,Attributes,CertPathBuilderResult,CertPathParameters,CertPathValidatorResult,CertSelector,CertStoreParameters,CharacterIterator,CRLSelector,Descriptor,GSSCredential,Name publicinterfaceCloneable 此类实现了Cloneable接口,以指示Object.clone()方法可以合法地对该类实例进行按字段复制。 如果在没有实现Cloneable接口的实例上调用Object的clone方法,则会导致抛出CloneNotSupportedException异常。 按照惯例,实现此接口的类应该使用公共方法重写Object.clone(它是受保护的)。 请参阅Object.clone(),以获得有关重写此方法的详细信息。 注意,此接口不包含clone方法。 因此,因为某个对象实现了此接口就克隆它是不可能的。 即使clone方法是反射性调用的,也无法保证它将获得成功。 clone protectedObjectclone() throwsCloneNotSupportedException 创建并返回此对象的一个副本。 “副本”的准确含义可能依赖于对象的类。 这样做的目的是,对于任何对象x,表达式: x.clone()! =x 为true,表达式: x.clone().getClass()==x.getClass() 也为true,但这些并非必须要满足的要求。 一般情况下: x.clone().equals(x) 为true,但这并非必须要满足的要求。 按照惯例,返回的对象应该通过调用super.clone获得。 如果一个类及其所有的超类(Object除外)都遵守此约定,则x.clone().getClass()==x.getClass()。 按照惯例,此方法返回的对象应该独立于该对象(正被复制的对象)。 要获得此独立性,在super.clone返回对象之前,有必要对该对象的一个或多个字段进行修改。 这通常意味着要复制包含正在被复制对象的内部“深层结构”的所有可变对象,并使用对副本的引用替换对这些对象的引用。 如果一个类只包含基本字段或对不变对象的引用,那么通常不需要修改super.clone返回的对象中的字段。 Object类的clone方法执行特定的复制操作。 首先,如果此对象的类不能实现接口Cloneable,则会抛出CloneNotSupportedException。 注意,所有的数组都被视为实现接口Cloneable。 否则,此方法会创建此对象的类的一个新实例,并像通过分配那样,严格使用此对象相应字段的内容初始化该对象的所有字段;这些字段的内容没有被自我复制。 所以,此方法执行的是该对象的“浅表复制”,而不“深层复制”操作。 Object类本身不实现接口Cloneable,所以在类为Object的对象上调用clone方法将会导致在运行时抛出异常。 返回: 此实例的一个副本。 抛出: CloneNotSupportedException-如果对象的类不支持Cloneable接口,则重写clone方法的子类也会抛出此异常,以指示无法复制某个实例。 另请参见: Cloneable
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 深入 理解 java 中的 clone