-
Notifications
You must be signed in to change notification settings - Fork 0
谨慎的覆写clone
Cloneable接口作为对象的一个mixin接口, 用于表示实现该接口的类对象允许克隆. Cloneable接口决定了Object中受保护的clone方法实现的行为: 如果一个类实现了Cloneable接口, Object的clone方法就会返回该对象的逐域拷贝, 否则调用clone方法将抛出CloneNotSupportedException异常.
Note:
(1)Cloneable接口并没有成功达到它应有的目的, 主要的缺陷在于:Cloneable接口中缺少一个clone方法, 而Object中的clone方法是受保护的. 如果不借助于反射, 就不能仅仅因为一个对象的类实现了Cloneable接口, 就可以调用clone方法. 即使是反射调用也可能会失败, 因为不能保证该对象一定具有可访问的clone方法.
接口的通常用于定义一种类型的对象所具有的行为, 而具体的行为实现通常由其子类决定. 但在Cloneable接口中却没有定义任何行为, 反而它还改变了超类中受保护方法的行为.
Cloneable接口中没有任何方法. 但实际上, 对于实现Cloneable接口的类, 我们总是期望它也提供一个功能适当的clone方法.
对于任何对象x, 表达式x.clone() != x将返回true; 并且, 表达式x.clone().getClass() == x.getClass()将返回true; 此外, 表达式x.clone().equals(x)将返回true. 但这些都不是绝对的要求.
拷贝对象往往会导致创建它的类一个新实例, 但它同时也会要求拷贝内部的数据结构. 但是这个过程没有调用构造器.
行为良好的clone方法可以调用构造器来创建对象, 构造之后再复制内部数据. 如果一个类是final类, 这个类就不会有子类, 所以在它的clone方法中调用构造器创建对象是一种合理选择.
在实践中, 程序员会假设: 如果他们扩展了一个类, 并且在子类的clone方法中调用super.clone(), 返回的对象将是该子类的实例. 超类能够提供这种功能的唯一途径, 返回一个通过super.clone()而得到的对象. 如代码示例4-1所示:
// 代码示例4-1 // 通过super.clone方式覆写clone方法的父类 public class OverrideCloneBySuperCloneSuper implements Cloneable { // 覆写clone方法 @Override public OverrideCloneBySuperCloneSuper clone() { try { return (OverrideCloneBySuperCloneSuper) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } } } // 通过super.clone方法覆写clone方法的子类 public class OverrideCloneBySuperCloneSub extends OverrideCloneBySuperCloneSuper { // 覆写clone方法 @Override public OverrideCloneBySuperCloneSub clone() { return (OverrideCloneBySuperCloneSub) super.clone(); } }
为代码示例4-1编写相应的单元测试代码如代码示例4-2所示:
// 代码示例4-2 // 通过super.clone方法覆写clone方法的测试方法 @Test public void testSuperClone() { OverrideCloneBySuperCloneSub sub = new OverrideCloneBySuperCloneSub(); OverrideCloneBySuperCloneSub vo = sub.clone(); logger.info("通过super.clone方式覆写clone方法得到克隆的对象为: {}", vo); }
测试结果如下:
通过super.clone方式覆写clone方法得到克隆的对象为: com.zachard.effective.java.general.method.OverrideCloneBySuperCloneSub@631330c
但是如果某个类中的clone方法返回一个由构造器创建的对象, 子类就可能会得到一个错误的类, 如代码示例4-3所示:
// 代码示例4-3 // 通过构造器覆写clone方法的父类 public class OverrideCloneByConstructorSuper implements Cloneable { // 通过构造器的方式覆写{@link #clone()}方法 @Override public OverrideCloneByConstructorSuper clone() { return new OverrideCloneByConstructorSuper(); } } // 通过super.clone覆写clone方法 public class OverrideCloneByConstructorSub extends OverrideCloneByConstructorSuper { // 通过{@link super#clone()}方式覆写{@link #clone()}方法 @Override public OverrideCloneByConstructorSub clone() { return (OverrideCloneByConstructorSub) super.clone(); } }
为代码示例4-3编写相应的单元测试代码如代码示例4-4所示:
// 代码示例4-4 // 存在通过构造器覆写clone方法的类测试方法 @Test public void testConstructor() { OverrideCloneByConstructorSub sub = new OverrideCloneByConstructorSub(); OverrideCloneByConstructorSub vo = sub.clone(); logger.info("父类通过构造器覆写clone方法, 子类通过super.clone方法覆写clone方法, " + "子类克隆的对象为: {}", vo); }
测试过程中抛出了ClassCastException异常, 如图4-1所示:
ClassCastException异常
图4-1
因此, 在覆写非final类中的clone方法时, 应该返回一个通过调用super.clone而得到的对象. 如果类的所有超类都遵守这条规则, 那么调用super.clone最终会调用Object的clone方法, 从而创建出正确的类实例.
如果一个类需要实现
Cloneable接口并覆写clone方法, 则必须保证当前类的所有超类提供了行为良好的clone实现.
(5.1) 如果类的每个域包含的是一个基本类型的值, 或是一个指向不可变对象(不可变对象指的是对象的属性不可变, 而不是当前域被final修饰)的引用, 那么super.clone()返回的对象可能正是你所需要的对象, 在这种情况下不需要再做进一步处理.
创建一个包含一个int类型, 一个String类型的类并覆写clone方法如代码示例5-1所示:
// 代码示例5-1 // 只包含基本类型或是不可变对象域的类覆写clone方法 public class OverrideBasicTypeClone implements Cloneable { //不可变对象类型域 private String name; //基本类型域 private int age; // 类构造器 public OverrideBasicTypeClone(String name, int age) { this.name = name; this.age = age; } // 通过直接返回{@link super#clone()}覆写{@link #clone()}方法 @Override public OverrideBasicTypeClone clone() { try { return (OverrideBasicTypeClone) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } } // 总是覆写{@link #toString()}方法 @Override public String toString() { return "姓名: " + name + "; 年龄: " + age; } // setter and getter }
编写相应的单元测试代码如代码示例5-2所示:
// 代码示例5-2 // 只包含基本类型及不可变对象类覆写clone方法测试 @Test public void testBasicTypeClone() { OverrideBasicTypeClone vo = new OverrideBasicTypeClone("zachard", 18); logger.info("初始对象为: {}", vo); OverrideBasicTypeClone cloneVo = vo.clone(); logger.info("克隆出来的对象为: {}", cloneVo); // 修改克隆对象的属性 cloneVo.setAge(20); cloneVo.setName("richard"); logger.info("修改克隆对象的属性, 克隆出来的对象为: {}", cloneVo); logger.info("修改克隆对象的属性, 初始对象为: {}", vo); }
测试结果如下:
初始对象为: 姓名: zachard; 年龄: 18
克隆出来的对象为: 姓名: zachard; 年龄: 18
修改克隆对象的属性, 克隆出来的对象为: 姓名: richard; 年龄: 20
修改克隆对象的属性, 初始对象为: 姓名: zachard; 年龄: 18
Note:
(1)代码示例5-1中覆写的clone方法返回的是OverrideBasicTypeClone类型, 而不是Object类型, 这种做法是合法的. 因为在Java 1.5之后, 覆盖方法返回的类型可以是被覆盖方法的返回类型的子类型. 这样有助于覆盖方法提供更多关于被返回对象的信息, 并且在客户端中不必进行转换. 记住: 永远不要让客户去做任何类库能够替客户完成的事情.
创建一个包含可变域的类并覆写其clone方法如代码示例5-3所示:
// 代码示例5-3 // 通过`super.clone`覆写包含可变对象类的`clone`方法 public class OverrideVariableObjectClone implements Cloneable { // 可变对象域 private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 5; // 类构造方法 public OverrideVariableObjectClone() { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } // 入栈 public void push(Object e) { ensureCapacity(); elements[size++] = e; } // 出栈 public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; // 消除过期引用 elements[size] = null; return result; } // 扩容 private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } // 总是覆写{@link #toString()}方法 @Override public String toString() { return "栈长度为: " + size + "; 元素为: " + Arrays.toString(elements); } // 覆写{@link #clone()}方法 @Override public OverrideVariableObjectClone clone() { try { return (OverrideVariableObjectClone) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }
编写相应的测试代码如代码示例5-4所示:
// 代码示例5-4 // 测试包含可变对象类的clone方法覆写 @Test public void testVariableObjectClone() { OverrideVariableObjectClone vo = new OverrideVariableObjectClone(); vo.push("Amy"); vo.push("Bob"); vo.push("Cary"); OverrideVariableObjectClone cloneVo = vo.clone(); logger.info("初始对象为: {}", vo); logger.info("克隆得到的对象为: {}", cloneVo); // 将克隆对象中的元素出栈 cloneVo.pop(); cloneVo.pop(); logger.info("克隆对象中元素出栈后, 克隆对象为: {}", cloneVo); logger.info("克隆对象中元素出栈后, 初始对象为: {}", vo); }
执行代码示例5-4中的代码, 结果如下:
初始对象为: 栈长度为: 3; 元素为: [Amy, Bob, Cary, null, null]
克隆得到的对象为: 栈长度为: 3; 元素为: [Amy, Bob, Cary, null, null]
克隆对象中元素出栈后, 克隆对象为: 栈长度为: 1; 元素为: [Amy, null, null, null, null]
克隆对象中元素出栈后, 初始对象为: 栈长度为: 3; 元素为: [Amy, null, null, null, null]
从结果可以看出, 当覆写的clone方法仅仅是返回super.clone得到的值时, 克隆对象中基本类型域具有正确的值, 但是指向可变对象引用的域将引用初始对象中的可变对象, 修改克隆对象中的可变对象会同时修改初始对象中的可变对象(将克隆对象中的元素出栈, 初始对象中的元素也被出栈, 但是初始对象中栈长度却未发生变化), 反之亦然.
Note:
clone方法就是另外一个构造器, 必须保证clone方法克隆出来的对象不会伤害到原始对象, 并确保正确地创建被克隆对象中的约束条件(invariant).
为了满足克隆对象与原始对象之间的约束, 在覆写clone方法时, 应该对包含可变对象引用的域递归调用clone方法, 针对代码示例5-3中的clone方法, 按代码示例5-5进行覆写:
// 代码示例5-5 // 覆写clone方法时递归调用包含可变对象域的clone方法 @Override public OverrideVariableObjectClone clone() { try { OverrideVariableObjectClone result = (OverrideVariableObjectClone) super.clone(); // 对于包含可变对象的域递归调用其clone方法 result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
再次运行代码示例5-4中, 结果如下:
初始对象为: 栈长度为: 3; 元素为: [Amy, Bob, Cary, null, null]
克隆得到的对象为: 栈长度为: 3; 元素为: [Amy, Bob, Cary, null, null]
克隆对象中元素出栈后, 克隆对象为: 栈长度为: 1; 元素为: [Amy, null, null, null, null]
克隆对象中元素出栈后, 初始对象为: 栈长度为: 3; 元素为: [Amy, Bob, Cary, null, null]
从结果中可以看出, 覆写clone方法时, 对于包含可变对象的域递归调用其clone方法, 可以消除克隆对象与原始对象之间的相互影响.
Note:
(1) 如果代码示例5-3中的elements域是final的,代码示例5-5中的方案就不能正常工作, 因为clone方法是被禁止给elements域赋新值的. 这是一个根本的问题:clone架构与引用可变对象的final域的正常用法是不兼容的, 除非在原始对象和克隆对象之间可以安全的共享此可变对象. 为了使类成为可克隆的, 可能有必要从某些域中去掉final修饰符.
TODO 这块有点不太理解, 可以参照
Java中的Hashtable.Entry类的clone实现及Hashtable中的clone实现.
如果clone调用了一个被覆盖的方法, 那么该方法所在的子类有机会修正它在克隆对象之前, 该方法先被执行, 这样很有可能会导致克隆对象和原始对象之间的不一致.
一般情况下, 这意味着要拷贝任何包含深层结构的可变对象, 并用指向新对象的引用代替原来指向这些对象的引用. 虽然, 这些内部拷贝操作往往可以通过递归地调用clone来完成, 但这通常不是最佳方法. 如果该类只包含基本类型的域, 或者指向不可变对象的引用, 那么多半的情况是没有域需要修正. 这条规则也有例外, 譬如, 代表序列号或其唯一ID值的域, 或者代表对象的创建时间的域, 不管这些域是基本类型还是不可变的, 他们也都需要被修正.
如果不想实现一个行为良好的clone方法, 最好提供某些其他的途径来代替对象拷贝, 或者干脆不提供这样的功能. 例如, 对于不可变类, 支持对象拷贝并没有太大的意义, 因为被拷贝的对象与原始对象并没有实质的不同.
代替覆写clone方法的方案包括: 提供一个拷贝构造器或拷贝工厂. 拷贝构造器只是一个构造器, 它唯一的参数类型是包含该构造器的类.
例如:
拷贝构造器: public Yum(Yum yum);
拷贝工厂: public static Yum newInstance(Yum yum);
(1) 拷贝构造器和拷贝工厂不依赖于某一种很有风险、语言之外的对象创建机制; 它们不要求遵守尚未制定好文档的规范; 它们不会与final域的正常使用发生冲突; 它们不会抛出不必要的受检查异常(checked exception); 它们不需要进行类型转换. 虽然不能把拷贝构造器或是拷贝工厂放到接口中, 但是由于Cloneable接口缺少一个公有的clone方法, 所以它也没有提供一个接口该有的功能. 因此, 使用拷贝构造器或拷贝工厂来代替clone方法时, 并没有放弃接口的功能特性.
(2) 拷贝构造器或者拷贝工厂可以带一个参数, 参数类型是通过该类实现的接口. 例如: 所有通用集合实现都提供了一个拷贝构造器, 它的类型为Collection或者Map. 基于接口的拷贝构造器和拷贝工厂, 允许客户选择拷贝的实现类型, 而不是强迫客户接受原始的实现类型. 例如: 假设有一个HashSet, 并且希望把它拷贝成一个TreeSet, clone方法无法提供这样的功能, 但是用拷贝构造器很容易实现.
既然Cloneable具有上述那么多问题, 可以肯定的说, 其他的接口都不应该扩展这个接口, 为了继承而设计的类也不应该实现这个接口. 由于它具有这么多缺点, 有些专家级程序员干脆从来不去覆盖clone方法, 也从来不去调用它, 除非拷贝数组. 对于一个专门为了继承而设计的类, 如果你未能提供行为良好的受保护的clone方法, 它的子类就不可能实现Cloneable接口.