《Effective Java》学习笔记13 Override clone judiciously

本栏是博主根据如题教材进行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.csdn.net/topics/392420556

看不明白大概有这么几个地方,罗列如下:

  1. 类似链式调用是因为它递归地调用父类的clone()方法吗
  2. 加粗的“错误”的类是指什么
  3. 不依赖Object.clone()这一句想表达什么

答:

  1. 构造函数是链式调用的,并且是强制执行的,也就是构造子类的前提必须是先构造父类。
    clone()方法原则上也是需要链式调用的,也就是必须调用父类的clone()方法,但是没有强制执行
  2. 关于“错误”可以参考下面的代码例子。在Test类中,由于父类的clone()方法没有对Sheep类的实例进行强制转换、返回Object型,而是直接返回了父类类型,而子类clone()直接调用父类clone()方法。这样做的后果就使得将clone()方法得到的实例进行转换时出现了向下转型的情况,非法,抛异常。可以与前面Student的相关代码中对比一下以便更深入地了解。
  3. Object的clone方法在执行时,如果该类没有实现Cloneable接口则会报错,也就是你如果不调用Object的clone方法,那么就相当于一个普通的方法,也就没有必要实现Cloneable接口。
  4. 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()相比,这种方式

  1.  无需依赖存在风险且依赖其他非Java语言实现的克隆机制,
  2.  不需要时时核对自己的实现方式是否与clone()的文档中所记载的相吻合(因为clone()没有强制的约束措施),
  3.  不会与final字段发生冲突,不要求强制转换,也不会抛出多余的检查型异常(checked exception)
  4.  可以接受一个接口类型的参数(这个类所实现的接口)。比如,为了方便可以将所有实现

 {@link java.util.Collection}的类的拷贝构造方法参数设置为Collection型。这种
 “基于接口的拷贝工厂or拷贝构造方法”(也称“转换工厂or转换构造方法”),使客户端能自由选择副本类型,
 而不用与被克隆的类相同。

比如,{@link java.util.TreeSet#TreeSet(Collection)}可以将原有对象复制并转换成任意实现了该接口的类,比如实现{@link java.util.HashSet}到{@link java.util.TreeSet}的转化。

最后一句,难用,不要扩展它,尽量少调用它,顶多在复制数组时稍微用一下。

 

全代码git地址:点我点我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值