• 《Effective Java》读书笔记09谨慎地覆盖clone方法


    一、Cloneable接口与Object.clone方法

    Cloneable接口的目的是作为对象的一个mixin接口(混合型接口),表明这样的对象允许克隆(clone)。遗憾的是Cloneable接口里并没有clone方法,其实它什么方法都没有,跟Serializable接口一样,都是占着茅坑不拉屎。它只是声明该对象可以被克隆,具体行为由类设计者决定。如果类设计者忘记提供一个良好的clone方法或根本不提供clone方法,那么类客户使用时必定会出错,这样Cloneable接口并没达到它的目的。

    Cloneable接口决定了Object中受保护的clone方法的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的极端非典型的用法,不值得效仿,因为它违背了接口的使用规范,改变了超类中受保护的方法的行为。

    Cloneable接口约束实现类及其所有超类都必须遵守一种语言之外的机制:无需调用构造器就可以创建对象。通常这种机制的通用约定是非常弱的。它要求实现类和父类必须提供一个Clone方法,在Clone方法中调用super.clone方法,已达到最终调用Object.clone方法来完成约定。如果有一层没有按照约定实现,那么该类的Clone功能将是潜在的灾难。

    二、Object中Clone方法的通用约定

    Clone方法用于创建和返回对象的一个拷贝,一般含义如下:

    1、对于任何对象x,表达式 x.clone()!=x 将会是true,并且表达式 x.clone().getClass() == x.getClass()将会是true,但这不是绝对要求。

    2、通常情况下,表达式 x.clone.equals(x)将会是true,同1一样这不是绝对要求。

    拷贝对象往往会导致创建它的类的一个新实例,但它同时也要求拷贝内部的数据接口,这个过程中没有调用构造器。

    该通用约定存在的问题:

    1、不调用构造器的规定太强硬

    行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。既然类是final的,不可变的,我当然可以调用构造器创建一个实例,甚至缓存起来(单例模式),等调用clone时直接返回该对象,这样效率更高。

    2、x.clone().getClass()通常应该等同于x.getClass()的规定太软弱

    在实践中,我们一般会假设:如果扩展一个类,并在子类中调用了super.clone,返回的对象就将是该子类的实例(我们要克隆的是子类而不是父类)。

    超类提供此功能的唯一途径是:返回一个通过调用super.clone而得到的对象。如果clone方法返回一个由构造器创建的对象,它就会得到错误的类(当前父类而不是想要的子类)。

    因此,如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果类的所有超类都遵守这条规则,那调用super.clone方法最终会调用Object.clone方法,从而创建正确类的实例,此机制类似于自动的构造器调用链,只不过它不是强制要求的。

    三、实现一个行为良好的clone方法

    从super.clone()中得到的对象有时接近于最终要返回的对象,有时会相差很远,这取决于该类的本质。

    1、每个域包含的只有基本类型或指向不可变对象的引用,这种情况返回的对象可能满足我们的需要,比如《读书笔记08》中的PhoneNumber类。在此,我们只需声明实现Cloneable接口,然后对Object中受保护的clone方法提供公有的访问途径:

     @Override public PhoneNumber clone() {
            try {
                return (PhoneNumber) super.clone();//协变返回类型,永远不要让客户去做任何类库能够替他完成的事情。
            } catch(CloneNotSupportedException e) {
                throw new AssertionError();  // Can't happen
            }
        }

    2、域中包含可变对象,如《读书笔记05》中的Stack类。

    如果想把该类做成cloneable的,如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例虽然size域具有正确的值(基本类型),但它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。

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

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

    假如我们仅仅像对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();
    		}
    	}

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

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

    私有类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;
    	}

    克隆复杂对象的最后一种办法:先调用super.clone,然后把结果对象中的所有域都设置成它的空白状态,然后调用高层(higher-level)的方法来重新产生对象的状态。这种方式简单,合理且优美,但运行速度通常没有"直接操作对象及其克隆对象的内部状态的clone方法"快。

    四、clone方法的替代品

    如果我们扩展一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。

    实现对象拷贝的好方法:

    提供一个拷贝构造器或拷贝工厂

    //Copy constructor
    public Yum(Yum yum);
    //Copy factory
    public static Yum newInstance(Yum yum);

    此方式优点:

    1、它们不依赖于某一种很有风险的,语言之外的对象创建机制。

    2、它们不要求遵守尚未制定好的文档规范。

    3、它们不会与final的正常使用发生冲突。

    4、它们不会抛出不必要的受检异常。

    5、它们不需要进行类型转换。

    6、它们可以带一个参数,参数类型是通过该类实现的接口,比如集合框架。

    五、最佳编程实践

    如果必须提供clone方法:

    1、clone方法不应该在构造的过程中,调用新对象中任何非final的方法,会造成克隆对象与原始对象的状态不一致。

    2、公有的clone方法应该省略CloneNotSupportException异常,因为这样使用起来更轻松。如果专门为了继承而设计的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected,抛出CloneNotSupportException异常,并且该类不应该实现Cloneable接口,以便子类可以自己决定是否实现它。

    3、用线程安全的类实现Cloneable接口,要记得它的clone方法必须得到很好地同步。

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

    5、使用拷贝构造器或拷贝工厂来代替clone方法

  • 相关阅读:
    lodop传统模板和文档式模板demo
    (方案,没有代码)直播服务器搭建,pc播放和手机播放
    银联在线网关支付测试运行的相关操作(个人笔记)
    docker停止mysql后容器消失
    打包发布ReactNative流程、提测
    工作中常用到的 Git 命令收藏
    React中函数式组件进行优化
    应该知道的TCP/IP
    移动应用消息推送及推送原理
    React中Refs的使用方法
  • 原文地址:https://www.cnblogs.com/xinyuyuanm/p/3003980.html
Copyright © 2020-2023  润新知