Effective Java读书笔记-谨慎的覆盖clone

Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆(clone)。Object的clone方法是受保护的,如果不借助反射(reflection),就不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法,即使是反射调用也可能会失败,因为不能保证该对象拥有可访问的clone方法。

Cloneable的作用:

它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。

通常情况下实现接口是为了表明类还能为它的客户做些什么,但实现Cloneable接口是改变超类中受保护方法的行为。

拷贝对象往往会导致创建它的类的一个实例,但它同时也会要求拷贝内部的数据结构。这个过程没有调用构造器。但是行为良好的clone会调用构造器来创建对象,构造之后在复制内部的数据。如果这个类是final的,clone甚至会返回一个由构造器创建的对象。

如果clone方法返回一个由构造器创建的对象,它就得到所有错误的类。因此,如果覆盖了非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果类的所有超类都遵守这条规则,那么调用super.clone最终会调用Object的clone方法,从而创建出正确类的实例。

注意:Cloneable接口并没有清楚地指明,一个类在实现这个接口时应该承担那些责任。实际上,对于实现了Cloneable的类,我们总是期望它也提供一个功能适当的公有的clone方法。通常情况下,除非该类的所有超类都提供了行为良好的clone实现,无论是公有的还是受保护的,否则,都不可能这么做。

假设希望在一个类中实现Cloneable,并且它的超类都提供了行为良好的clone方法。从super.clone中得到的对象可能会接近与最终要返回的对象。例如有一个类phoneNumber,你所需要做的,除了声明实现了cloneable之外,就是对Object中受保护的clone方法提供公有的访问途径:

@Override public phoneNumber clone(){
    try{
        return (phoneNumber)super.clone();
    }catch(CloneNotSupportedException e){
        throw new AssertionError();
    }
}

Java1.5中引进了协变返回类型(covariant return type)作为泛型。换句话说,目前覆盖方法的返回类型可以是被覆盖方法返回类型的子类。这样有助于覆盖方法提供工多关于被返回对象的信息,并且在客户端中不必进行转换。这里体现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情。

如果对象中的包含域引入了可变对象,那么使用上述方法将会导致灾难性的后果。例如有一个Stack类,其中有一个size属性代表栈的长度,还有一个elements域,代表栈中的每个元素。使用super.clone方法将会返回对象,它的size域会有正确的值,但是elements域会有与原先实例相同的数组。修改原始的实例会破坏克隆对象中的约束条件,反之亦然。很快你就会发现这个程序将会返回毫无意义的结果,或者抛出NullPointerException异常。

clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件(invariant)。为了使Stack类中的clone方法正常工作,它必须拷贝栈的内部信息。最容易的做法是,在elements数组中递归调用clone:

@Override public Stack clone(){
    try{
        Stack result = (Stack)super.clone();
        result.elements = elements.clone();
        return result;
    }catch(CloneNotSupportedException e){
        throw new AssertionError();
    }
}

注意:我们不需要将elements.clone()的结果转换成Object[]。从java1.5开始,在数组上调用clone返回的数组,其编译时的类型与被克隆的数组类型相同。如果elements是final修饰的,那么上述方案就无法正常工作。clone架构与引用了可变对象的final域的正常使用是不相兼容的,除非原始对象和克隆对象之间可以安全的共享此可变对象。为了使类成为可克隆的,可能有必要从某些域中去掉final修饰符。

递归的调用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;

    }
}

之后克隆这个散列桶数组,类似Stack那样的操作

@Override public HashTable clone(){
    try{
        HashTable result = (HashTable)super.clone();
        result.buckets = buckets.clone();
        return result;
    }catch(CloneNotSupportedException e){
        throw new AssertionError();
    }
}

虽然被克隆对象有它自己的散列桶数组,但是这个数组引用的链表与原始对象是一样的,从而很容易引起克隆对象和原始对象中的不确定行为。为了修正这个问题必须单独地拷贝并组成每个桶的链表。下面是一种常见的做法。

public class HashTable implements Cloneable{
    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;
        }

        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();
        }
    }
}

Entry类中的深度拷贝方法调用其自身,以便拷贝整个链表。虽然这种方法很灵活,如果散列桶不是很长的话,也会工作的很好,但是,这样克隆一个链表并不是一种好的办法,因为针对链表中的每一个元素,它都要消耗一段栈空间。如果链表比较长,就容易产生栈的溢出。为了避免这种情况的发生,可以在deepcopy方法中使用迭代来代替递归。

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;
}

克隆复杂对象的最后一种方法是,先调用super.clone方法,然后把结果对象中所有域都设置为它们的空白状态,然后调用高层的方法来重新产生对象的状态。

如同构造器一样,clone方法不应再在构造的过程中,调用新对象中任何非final的方法。如果clone调用了一个被覆盖的方法,那么该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会被先执行,这样很可能导致克隆对象与原始对象间的不一致。因此,使用put方法添加元素的时候,要么是final的,要么是私有的。

注意:如果决定使用线程安全的类实现Cloneable接口,要记得它的clone方法必须得到很好的同步。Object的clone方法没有同步,因此即使很满意,可能也必须编写同步的clone方法来调用super.clone()。

简而言之,所有实现了Cloneable的接口的类都应该有一个公有的方法来覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。

另一种实现拷贝对象比较好的办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值