1.序
Object 中 clone 方法的定义是:
protected native Object clone() throws CloneNotSupportedException;
调用clone()方法需要对象实现Cloneable接口,该接口决定了Object中受保护的clone方法的实现行为:
- 如果一个类实现了Cloneable接口,Object的clone方法返回该对象的逐域拷贝;
- 如果一个类未实现Cloneable接口,则该对象就会抛出CloneNotSupportedException异常。
对于Cloneable接口,它改变了超类中受保护的方法的行为。
2.clone 方法规范
如果实现Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制:无需调用构造器就可以创建对象。
clone 方法的通用约定是非常弱的,下面是摘自 Object 规范中的约定内容
Clone方法用于创建和返回对象的一个拷贝,一般含义如下:
1、对于任何对象x,表达式 x.clone()!=x 将会是true,并且表达式 x.clone().getClass() == x.getClass() 将会是true,但这不是绝对要求。
2、通常情况下,表达式 x.clone.equals(x) 将会是true,同1一样这不是绝对要求。
3.约定存在的问题
拷贝对象往往会导致创建它的类的一个新实例,但它同时也要求拷贝内部的数据接口,这个过程中没有调用构造器。让我们看看以上约定存在的问题:
3.1 不调用构造器的规定太强硬
行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。既然类是final的,不可变的,当然可以调用构造器创建一个实例,甚至缓存起来(单例模式),等调用clone时直接返回该对象,这样效率更高。
3.2 x.clone().getClass()通常应该等同于x.getClass()的规定太软弱
在实践中,我们一般会假设:如果扩展一个类,并在子类中调用了super.clone,返回的对象就将是该子类的实例(我们要克隆的是子类而不是父类)。
super.clone(),这个操作主要是来做一次bitwise copy( binary copy ),即浅拷贝,他会把原对象完整的拷贝过来包括其中的引用。这样会带来问题,如果里面的某个属性是个可变对象,那么原来的对象改变,克隆的对象也跟着改变。所以在调用完super.clone()后,一般还需要重新拷贝可变对象。
超类提供此功能的唯一途径是:返回一个通过调用super.clone而得到的对象。如果clone方法返回一个由构造器创建的对象,它就会得到错误的类(当前父类而不是想要的子类)。
因此,如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果类的所有超类都遵守这条规则,那调用super.clone方法最终会调用Object.clone方法,从而创建正确类的实例,此机制类似于自动的构造器调用链,只不过它不是强制要求的。
综上:
- 不可变的类永远都不应该提供 clone 方法
- Cloneable 结构与引用可变对象的 final 域的正常做法是不相兼容的。
- clone 方法是浅拷贝(只拷贝一层),对类所引用的对象需手动拷贝
来看一下浅拷贝和深拷贝的示例:
public class Student implements Cloneable{
String name;
int age;
public Student(String name,int age){
this.name = name;
this.age = age;
}
public Object clone(){
Object o = null;
try{
o = (Student)super.clone();//Object 中的clone()识别出你要复制的是哪一个对象
}catch(CloneNotSupportedException e){
System.out.println(e.toString());
}
return o;
}
public static void main(String[] args){
Student s1=new Student("zhangsan",18);
Student s2=(Student)s1.clone();
System.out.println("克隆后s2:name="+s2.name+","+"age="+s2.age);
s2.name="lisi";
s2.age=20;
//修改学生2后,不影响学生1的值。
System.out.println("克隆修改后s1:name="+s1.name+","+"age="+s1.age);
System.out.println("克隆修改后s2:name="+s2.name+","+"age="+s2.age);
}
}
这时候,若是类的每一个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么被返回的对象则正是所须要的对象,只须要简单地调用super.clone() 而不用作进一步的处理。可是!若是对象中包含其余对象的引用时,那么只是简单的clone就没法作到彻底的克隆了,下面的例子咱们就能够体会到接口
class Professor {
String name;
int age;
Professor(String name,int age){
this.name=name;
this.age=age;
}
}
public class Student implements Cloneable{
String name;// 常量对象。
int age;
Professor p;// 学生1和学生2的引用值都是同样的。
Student(String name,int age,Professor p){
this.name=name;
this.age=age;
this.p=p;
}
public Object clone(){
Student o=null;
try{
o=(Student)super.clone();
}catch(CloneNotSupportedException e){
System.out.println(e.toString());
}
return o;
}
public static void main(String[] args){
Professor p=new Professor("wangwu",50);
Student s1=new Student("zhangsan",18,p);
Student s2=(Student)s1.clone();
System.out.println("克隆后s1:name="+s1.p.name+","+"age="+s1.p.age);
System.out.println("克隆后s2:name="+s2.p.name+","+"age="+s2.p.age);
s2.p.name="lisi";
s2.p.age=30;
System.out.println("克隆后s1:name="+s1.p.name+","+"age="+s1.p.age);
System.out.println("克隆后s2:name="+s2.p.name+","+"age="+s2.p.age);
}
}
从结果上咱们能够看出,s2对s1进行克隆时,对s1的属性Professor p并无进行克隆,致使s1和s2对其引用指向同一个,这会形成s2若改变了值,s1则也被动改变了。那应该如何实现深层次的克隆,即修改s2的教授不会影响s1的教授?其实很简单,只须要对Professor进行修改,以下所示便可get
class Professor implements Cloneable{
String name;
int age;
Professor(String name,int age){
this.name=name;
this.age=age;
}
public Object clone(){
Object o = null;
try{
o = super.clone();
}catch(CloneNotSupportedException e){
System.out.println(e.toString());
}
return o;
}
}
修改Professor后,还须要在Student的clone方法中加入一句代码:o.p=(Professor)p.clone();
public Object clone(){
Student o=null;
try{
o=(Student)super.clone();
}catch(CloneNotSupportedException e){
System.out.println(e.toString());
}
o.p=(Professor)p.clone();
return o;
}
看到结果就如咱们所但愿的那样。所以,在使用clone时,必定要分清须要克隆的对象属性。
4. 如何实现一个行为良好的clone方法
从super.clone()中得到的对象有时接近于最终要返回的对象,有时会相差很远,这取决于该类的本质。
4.1 每个域包含的只有基本类型或指向不可变对象的引用
这种情况返回的对象可能满足我们的需要,比如《读书笔记08》中的PhoneNumber类。在此,我们只需声明实现Cloneable接口,然后对Object中受保护的clone方法提供公有的访问途径:
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();//协变返回类型,永远不要让客户去做任何类库能够替他完成的事情。
} catch(CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
4.2 域中包含可变对象
如《读书笔记05》中的Stack类。 如果想把该类做成cloneable的,如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例虽然size域具有正确的值(基本类型),但它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。
clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
递归调用clone:
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
//递归调用clone。如果elements是final的,则需要把final去掉,因为final使得elements域不能被赋新值。
//另外,在数组上调用clone返回是数组,并且它的编译时类型与被克隆数组的类型相同
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
4.3 变量中的变量之深度拷贝
有时候递归地调用 clone还不够。比如,自己实现一个散列表并为它编写clone方法,它的内部数据包含一个散列桶数组,每个散列桶都指向"键-值"对链表的第一个项,如果桶是空的,则为null。出于性能方面的考虑,该类实现了自己的轻量级单向链表,而没有使用java内部的java.util.LinkedList,具体类实现如下:
public class HashTable implements Cloneable{
private Entry[] buckets = ...;
private static class Entry{
final Object key;
Object value;
Entry next;
Entry(Object key,Object value,Entry next){
this.key = key;
this.value = value;
this.next = next;
}
....//Remainder omitted13}
假如我们仅仅像对Stack那样递归地克隆这个散列桶数组,如下:
//Broken - results in shared internal state!
@Override public HashTable clone(){
try{
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
虽然被克隆的对象有它自己的散列桶数组,但这个数组引用的链表与原始对象是一样的,从而容易引起克隆对象和原始对象中不确定的行为。 为修正该问题,需要单独地拷贝并组成每个桶的链表,下面是一种常用做法:
// Recursive clone method for class with complex mutable state
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// Recursively copy the linked list headed by this Entry
Entry deepCopy() {
return new Entry(key, value,
next == null ? null : next.deepCopy());
}
}
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
... // Remainder omitted
}
私有类HashTable.Entry被加强了,支持深度拷贝。此方法虽然很灵活,但如果链表比较长,则很容易导致栈溢出,列表中的每个元素都要消耗一段栈空间的。可以采用迭代来代替递归,如下:
//Iteratively copy the linked list headed by this Entry
Entry deepCopy(){
Entry result = new Entry(key,value,next);
for(Entry p = result; p.next!=null; p=p.next)
p.next = new Entry(p.next.key,p.next.value,p.next.next);
return result;
}
4.4 复杂对象的克隆方法
克隆复杂对象的最后一种办法:先调用super.clone,然后把结果对象中的所有域都设置成它的空白状态,然后调用高层(higher-level)的方法来重新产生对象的状态。这种方式简单,合理且优美,但运行速度通常没有"直接操作对象及其克隆对象的内部状态的clone方法"快。
5.更好的做法
提供一个拷贝构造器或拷贝工厂来代替 clone 方法
拷贝构造器:
public class MyObject {
public String field01;
public MyObject() {
}
public MyObject(MyObject object) {
this.field01 = object.field01;
}
}
拷贝静态工厂:
public class MyObject {
public String field01;
public MyObject() {
}
public static MyObject newInstance(MyObject object) {
MyObject myObject = new MyObject();
myObject.field01 = object.field01;
return myObject;
}
}
优点
- 其不依赖于某一种很有风险的、语言之外的对象创建机制;
- 其不遵守尚未制定好的文档规范;
- 其不会与final域的正常使用发生冲突;
- 其不会抛出不必要的受检查异常;
- 其不需要类型转换;
- 采用其代替clone方法时,并没有放弃接口功能特性。
6.总结
如果必须提供clone方法:
-
1、clone方法不应该在构造的过程中,调用新对象中任何非final的方法,会造成克隆对象与原始对象的状态不一致。
-
2、公有的clone方法应该省略CloneNotSupportException异常,因为这样使用起来更轻松。如果专门为了继承而设计的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected,抛出CloneNotSupportException异常,并且该类不应该实现Cloneable接口,以便子类可以自己决定是否实现它。
-
3、用线程安全的类实现Cloneable接口,要记得它的clone方法必须得到很好地同步。
-
4、任何实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此方法首先调用super.clone,然后修正任何需要修正的域。
既然所有的问题都与 Cloneable 接口有关,新的接口就不应该扩展这个接口,新的可扩展的类也不应该实现这个接口。复制功能最好有构造器或工厂提供。(除数组)
7.参考文献
https://blog.albumenj.cn/archives/128
https://www.daimajiaoliu.com/daima/47dd7ed0f900410
http://www.javashuo.com/article/p-hagwozym-hd.html
https://www.jianshu.com/p/acbc3fb53c62
本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!
1.计算机网络----三次握手四次挥手
2.梦想成真-----项目自我介绍
3.你们要的设计模式来了
4.一字一句教你面试“个人简介”
5.接近30场面试分享