本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错和运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch和各路翻译者们,以及为我提供可参考博文的博主们。
谨慎地重新clone()方法
Cloneable接口
Cloneable接口与其他接口不同,它里面啥都没有,仅仅是为了表示该类可以被克隆。所有单独只有它自己,还不能够实现实例拷贝的目的。它实际上是一个混合接口(mixin interface),由于自身没有clone()方法,而Object中的clone方法又是受保护的,因此只能借助反射;但反射调用也有可能会失败,因为无法保证该对象一定具有可访问的clone方法。尽管如此,这种做法仍然在被广泛使用,所以下面讨论如何编写良好的clone方法,什么时候需要编写这个方法,以及它的一些替代方式。
那么这个自身啥都没有的接口都做了些啥?它决定了Object中受保护的clone方法的行为。如果某对象implements了Cloneable接口,那么调用其clone方法就会返回该对象的逐域拷贝(field-to-field copy),否则就会抛出CloneNotSupportedException.这是一种接口的超级超级非典型使用方式,因为这个接口并没表明实现它的类应做什么,而是改变了超类(Object类)受保护方法的行为,没有必要去练习这种使用方式。
clone()方法的使用
虽然没有强制规定,但实际上实现了Cloneable接口的类应该提供一个public Object clone()方法,而为了实现这个方法,它跟它的所有子类都必须遵守一个复杂、非强制(unenforceable)、缺少文档说明的协议,然后得到一种危险,脆弱,不符合Java语言规范的机制:不通过构造方法就创建对象。
clone()方法的通用规约
clone()的通用规约是非常弱的,摘录如下:
1.创建并返回该对象的副本,副本对象与原对象的关系视情况而定,但一般情况下应有
x.clone() != x ;
x.clone().getCLass() == x.getClass();
x.clone().equals(x) == true ;
但这几条都不是硬性要求
2.一般而言,返回的实例应该通过调用父类的super.clone()获得(Object类除外,因为clone()是Object类中定义的方法),只要该类以及其所有父类全部遵守规约,那么就自然地会满足
x.clone().getCLass() == x.getClass().
3.一般而言,副本对象应独立于被克隆对象。
为了实现这一点,可能会需要对super.clone()所return的实例做一些修改,然后返回这个修改后的实例。(比如,复制完整的数据结构,更改对其引用等)。这里要做一点说明:如果被克隆对象中有非基本类型对象,而非基本类型对象又没有继承Cloneable接口,那么克隆时只会将引用复制过去,即克隆前后的两个对象共享这个非基本类型的变量,这也被称为浅拷贝(Shallow)。比如这个例子:
/**
* 用于说明clone()的深拷贝与浅拷贝
*
* 如果要避免clone()方法对非基本类型仅复制其引用,就要重写clone(),
* 对每一个非基本类型调用其clone方法(该非基本类型应implements Cloneable接口)
*/
public final class Student implements Cloneable {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
//implements了Cloneable接口并重写了clone()方法的深拷贝Teacher类
DeepTeacher teacher1 = new DeepTeacher("张老师", 28);
//未implements Cloneable接口的普通Teacher类
ShallowTeacher teacher2 = new ShallowTeacher("李老师", 40);
@Override
public Object clone() throws CloneNotSupportedException{
//在这里进行强转而不是return一个Object类,因为“能由类库完成的工作就不要让客户端完成”
//Java1.5后的新机制,协变返回类型(covariant return type),可返回被覆盖方法返回类型的子类,
//以便于提供更多关于返回对象的信息
//但这个地方是因为有“该类为final型”前提,才敢这么干的,
//否则可能会在继承时出现类型转换问题(见下面深浅复制的说明)
Student newStudent = (Student) super.clone();
newStudent.teacher1 = (DeepTeacher) teacher1.clone();
return newStudent;
}
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student("小赵" , 18);
Student s2 = (Student) s1.clone();
System.out.println("s1: " + s1);
System.out.println("克隆后的s2:" + s2);
System.out.println(s2.teacher1);
System.out.println(s2.teacher2);
s1.teacher1.setAge(s1.teacher1.getAge() + 1);
s1.teacher2.setAge(s1.teacher2.getAge() + 1);
System.out.println("一年以后(对s1中两位老师年龄+1)并显示s2的教师信息:");
System.out.println(s2.teacher1);
System.out.println(s2.teacher2);
}
}
打印结果:
s1: Student@1540e19d
克隆后的s2:Student@677327b6
DeepTeacher@name :张老师 age : 28
ShallowTeacher@name :李老师 age : 40
一年以后(对s1中两位老师年龄+1)并显示s2的教师信息:
DeepTeacher@name :张老师 age : 28
ShallowTeacher@name :李老师 age : 41
可以看到,当修改被克隆对象s1中两位教师的年龄时,未implements Cloneable接口的普通类ShallowTeacher年龄同时发生改变,而实现深拷贝的对象DeepTeacher则没有。
注意Student.clone()中,由于在Object.clone()中声明了有可能会抛CloneNotSupportedException异常,这是一个检查型异常,因此会提示开发者去处理它。
这里顺势在放上两种Teacher代码的同时,正式介绍一下深拷贝(Deep Copy)与浅拷贝(Shallow Copy)两种拷贝的区别。
深拷贝与浅拷贝
非基本类型实例的深拷贝需要让类implements Cloneable接口,然后重写clone()方法。这样,其他拥有该类型成员变量的类在被clone()方法复制时,会递归地调用成员变量的clone()方法,递归地按照各成员变量的clone()步骤进行复制。
/**
* 深层复制,需要implements cloneable接口并重写Object.clone()
*/
public class DeepTeacher implements Cloneable {
String name;
int age;
public DeepTeacher(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
@Override
public String toString() {
return getClass().getName() + "@" + "name :" + name + " age : " + age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
而当使用clone()方法的实例中,部分成员变量没有implements Cloneable接口并重写clone()方法的话,那么复制时就会仅仅复制该成员变量的引用。也就是说,复制出来的新对象中,这些成员变量跟原来对象指向的是同一个域,在原来实例中对这个域进行修改也会影响到新复制出来的实例。
/**
* 浅复制
*/
public class ShallowTeacher {
String name;
int age;
ShallowTeacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return getClass().getName() + "@" + "name :" + name + " age : " + age;
}
}
深拷贝机制和构造函数链有点类似,只是它没有被强制执行:
如果一个类的克隆方法返回一个实例,这个实例不是通过调用super.clone()而是通过构造方法获得的,编译器虽然不会报错,但如果该类的子类调用了super.clone(),那么就会返回错误的类,则会导致子类的clone()也出现问题。如果重写clone()的类是final型,不会有子类,就不需要考虑这个问题,但如果声明为final的类,其clone()方法又不调用Super.clone(),那么由于不依赖于Object.clone(),其实也没必要implements Cloneable接口。
当时看英文版的时候这一段就看得十分蒙逼,然后找了半天没找着关于这段的理解,最后自己发了个帖,在大佬的热心帮助下明白了。原帖地址可以看这里:https://bbs.youkuaiyun.com/topics/392420556
看不明白大概有这么几个地方,罗列如下:
- 类似链式调用是因为它递归地调用父类的clone()方法吗
- 加粗的“错误”的类是指什么
- 不依赖Object.clone()这一句想表达什么
答:
- 构造函数是链式调用的,并且是强制执行的,也就是构造子类的前提必须是先构造父类。
clone()方法原则上也是需要链式调用的,也就是必须调用父类的clone()方法,但是没有强制执行。 - 关于“错误”可以参考下面的代码例子。在Test类中,由于父类的clone()方法没有对Sheep类的实例进行强制转换、返回Object型,而是直接返回了父类类型,而子类clone()直接调用父类clone()方法。这样做的后果就使得将clone()方法得到的实例进行转换时出现了向下转型的情况,非法,抛异常。可以与前面Student的相关代码中对比一下以便更深入地了解。
- Object的clone方法在执行时,如果该类没有实现Cloneable接口则会报错,也就是你如果不调用Object的clone方法,那么就相当于一个普通的方法,也就没有必要实现Cloneable接口。
-
class Sheep implements Cloneable { Sheep(String name)... public Object clone() { return new Sheep(this.name); // bad, doesn't cascade up to Object } } class WoolySheep extends Sheep { public Object clone() { return super.clone(); } } class Test { public static void main(String[] args) { WoolySheep dolly = new WoolySheep("Dolly"); //Exception in thread "main" java.lang.ClassCastException: WoolySheep clone = (WoolySheep)(dolly.clone()); } }
如果要写一个实现Cloneable接口的类,首先要调用父类的clone()方法,获取一个功能正常齐全的父类副本,此时子类中与父类的对应字段其值已经与父类相同了。倘若你的子类中只有基本类型或者不可变类型的成员变量,就跟上面规约第3条类似那样,那么这时候这个super.clone()返回的对象就已经满足你的需求了;但要注意:对于不可变对象,不应该提供关于这种对象的clone方法,因为反正不可变,直接拿去用就好,clone一下反而浪费时间。
而对于可变的非基本类型,如果仅仅简单地return super.clone(),那么就会出现跟Student相同的问题:仅仅克隆了非基本类型对象的引用,克隆出来的对象会受对于原对象的操作的影响。因此需要在重写clone方法时递归地调用其中可变非基本类型实例的super.clone()方法,创建其中对应成员变量的副本,就像Student.clone()那样:
public Object clone() throws CloneNotSupportedException{
//final型类因此无需考虑继承导致的类型转换异常问题
Student newStudent = (Student) super.clone();
newStudent.teacher1 = (DeepTeacher) teacher1.clone();
return newStudent;
}
注意事项
对于数组的拷贝
注意无需将数组实例(array)的super.clone()结果强制转换为Object[]。对于数组,其执行clone()的返回值在运行或编译时与其本身是相同的,因此clone()方法也是复制数组的首选方式。实际上,复制数组应该是clone()唯一做得不错的地方了。
对于final型成员的拷贝
另外还要注意,如果成员变量被声明为final型,那么由于clone过程中需要为其赋值,因此会发生错误,进而抛出异常。这个应该不难理解,与之类似的情况还有序列化(serialization)。因此,为了能使这个类的实例能被顺利地序列化,往往需要去掉一些成员变量的final修饰符。
对于复杂对象的拷贝
但有时候,仅仅是递归地调用父类的clone()方法还不够,例如这个下面这个:
/**
* 用于演示为什么有时候仅仅递归地调用父类的clone()方法还不够
*
* 该类包含一个数组{@link #buckets},数组中每个元素都是{@link Entry}类型键值对(K-V)链表。
* (为了提高性能,该类实现了自己的轻量级单链接列表,而没有使用{@link java.util.LinkedList})
*
* 但是,这种复制方式仅仅将数组中对于Entry的引用复制了过来,这就导致了克隆前后对象共用相同的bucket[]
*
* @author LightDance
*/
public class BadHashTable implements Cloneable{
private Entry[] buckets;
BadHashTable(Entry[] buckets) {
this.buckets = buckets;
}
@Override
protected Object clone(){
BadHashTable hashTable = null;
try {
hashTable = (BadHashTable) super.clone();
hashTable.buckets = buckets.clone();
return hashTable;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
private static class Entry {
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
@Override
public String toString() {
return "key: " + key + "; value :" + value;
}
//...
}
public static void main(String[] args) {
Entry[] list = new Entry[3];
for (int i = 0; i < 3; i++) {
list[i] = new Entry(i , i + " - 1" , new Entry(i , i + " - 2" , null));
}
BadHashTable hashTable1 = new BadHashTable(list);
BadHashTable hashTable2 = (BadHashTable) hashTable1.clone();
hashTable1.buckets[0].next = null;
//结果为null
System.out.println(hashTable2.buckets[0].next);
//结果为true
System.out.println(hashTable1.buckets[1].equals(hashTable2.buckets[1]));
}
}
于是引申出如下形式的复杂对象克隆方式:通过在Entry中添加自己定义的深层复制方法,防止复制对对象的引用从而造成不稳定性。
/**
* 使用深层复制,解决{@link BadHashTable}中克隆前后对象共用相同的bucket[]而造成的不稳定性
* <p>
* 本类在Entity中提供了两种复制方式,第一种是递归调用自身,但这种方式当递归次数特别多时容易爆栈,
* 参考{@link Entry#deepCopy1()};;
* 另一种是用迭代方式构建,这种方式可以防止前一个方式所述的问题{@link Entry#deepCopy2()}
*
* @author LightDance
*/
public class RecommendHashTable implements Cloneable {
private Entry[] buckets;
public RecommendHashTable(Entry[] buckets) {
this.buckets = buckets;
}
@Override
public RecommendHashTable clone() {
try {
RecommendHashTable result = (RecommendHashTable) 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].deepCopy1();
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
//...
private static class Entry {
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
/**递归方式的深层克隆(deep clone)*/
Entry deepCopy1() {
return new Entry(key, value, next == null ? null : next.deepCopy1());
}
/**迭代方式的深层克隆(deep clone)*/
Entry deepCopy2() {
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;
}
@Override
public String toString() {
return "key: " + key + "; value :" + value;
}
}
}
此外,还有另一种方式去解决上述问题:
/**
* 克隆复杂可变对象的最后一种方法是使用super.clone()之后,将获取到的对象中所有字段初始化,
* 然后调用更高级别的方法重新为该实例的字段赋值。比如这里,buckets = new Entry[];
* 然后调用put(key,value)方法(没加具体逻辑)为其中的每一个Entry赋值
*
* @author LightDance
*/
public class AnotherHashTable {
private Entry[] buckets;
AnotherHashTable(Entry[] buckets) {
this.buckets = buckets;
}
@Override
protected Object clone(){
AnotherHashTable hashTable = null;
try {
hashTable = (AnotherHashTable) super.clone();
hashTable.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
hashTable.buckets[i].put(this.buckets[i].deepCopy1());
//大意为将buckets中
}
return hashTable;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
private static class Entry {
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
Entry deepCopy1() {
return new Entry(key, value, next == null ? null : next.deepCopy1());
}
public void put(Entry entry){
//...
}
@Override
public String toString() {
return "key: " + key + "; value :" + value;
}
//...
}
}
这种方式虽然简洁易读,但与跟“克隆”这一概念好像没什么关系,因为这种方式用“重新生成”取代了数组的clone(),破坏了clone()方法的结构
不应在clone()中调用可以被重写的方法
与构造函数类似,clone()也不应该在其方法体中调用可以被重写的方法。否则其子类就仍然有机会调用那个过时的、被重写的方法,进而损坏克隆前后对象的结构。所以,前面被调用的RecommendHashTable.Entry#deepCopy1()以及hashtable.AnotherHashTable.Entry#put(AnotherHashTable.Entry)这样的方法,其实都应该加上final或者private字段进行保护。(如果加上private关键字,那么它可以算是非final方法的“辅助方法”helper method)
虽然{@link Object#clone()}的声明中说可能会抛出CloneNotSupportedException,但是重写这个方法时就没必要再加一句声明了,因为不抛出这个检查型异常可能会更容易编程。
类层次结构中的cloneable声明
设计用于继承的类时有两种选择(允许继承并提供文档,或者禁止继承),但无论哪一种,这个类都不应该implements Cloneable. 可以模仿Object.clone()这样声明:
protected Object clone() throws CloneNotSupportedException;
这样子类就可以自己决定是否需要implements Cloneable了;或者可以干脆禁止子类继承,扔个异常出来:
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
多线程中的同步问题
还有一点细节需要注意,如果写了实现Cloneable的线程安全类,应注意在Object类中,clone方法并没有被加上synchronized锁,因此需要像其他方法一样处理好同步锁的问题,或许需要实现一个返回super.clone()的synchronized clone()方法。
总结
综上所述,clone()这东西很难用,因此能替则替。更好的复制对象方式是提供一个“拷贝构造方法”(copy constructor)或者“拷贝工厂”(copy factory),接受一个类型为方法所在的类的参数,比如:
public OverrideClone(OverrideClone object) {
//...
}
与clone()相比,这种方式
- 无需依赖存在风险且依赖其他非Java语言实现的克隆机制,
- 不需要时时核对自己的实现方式是否与clone()的文档中所记载的相吻合(因为clone()没有强制的约束措施),
- 不会与final字段发生冲突,不要求强制转换,也不会抛出多余的检查型异常(checked exception)
- 可以接受一个接口类型的参数(这个类所实现的接口)。比如,为了方便可以将所有实现
{@link java.util.Collection}的类的拷贝构造方法参数设置为Collection型。这种
“基于接口的拷贝工厂or拷贝构造方法”(也称“转换工厂or转换构造方法”),使客户端能自由选择副本类型,
而不用与被克隆的类相同。
比如,{@link java.util.TreeSet#TreeSet(Collection)}可以将原有对象复制并转换成任意实现了该接口的类,比如实现{@link java.util.HashSet}到{@link java.util.TreeSet}的转化。
最后一句,难用,不要扩展它,尽量少调用它,顶多在复制数组时稍微用一下。
全代码git地址:点我点我