《Effective Java》阅读笔记 11谨慎地覆盖clone

345 阅读10分钟

1.序

Object 中 clone 方法的定义是:

protected native Object clone() throws CloneNotSupportedException;

调用clone()方法需要对象实现Cloneable接口,该接口决定了Object中受保护的clone方法的实现行为:

  • 如果一个类实现了Cloneable接口,Object的clone方法返回该对象的逐域拷贝;
  • 如果一个类未实现Cloneable接口,则该对象就会抛出CloneNotSupportedException异常。

对于Cloneable接口,它改变了超类中受保护的方法的行为。

2.clone 方法规范

如果实现Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制:无需调用构造器就可以创建对象。

clone 方法的通用约定是非常弱的,下面是摘自 Object 规范中的约定内容

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

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

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

3.约定存在的问题

拷贝对象往往会导致创建它的类的一个新实例,但它同时也要求拷贝内部的数据接口,这个过程中没有调用构造器。让我们看看以上约定存在的问题:

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

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

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

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

super.clone(),这个操作主要是来做一次bitwise copy( binary copy ),即浅拷贝,他会把原对象完整的拷贝过来包括其中的引用。这样会带来问题,如果里面的某个属性是个可变对象,那么原来的对象改变,克隆的对象也跟着改变。所以在调用完super.clone()后,一般还需要重新拷贝可变对象。

超类提供此功能的唯一途径是:返回一个通过调用super.clone而得到的对象如果clone方法返回一个由构造器创建的对象,它就会得到错误的类(当前父类而不是想要的子类)。 因此,如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果类的所有超类都遵守这条规则,那调用super.clone方法最终会调用Object.clone方法,从而创建正确类的实例,此机制类似于自动的构造器调用链,只不过它不是强制要求的。

综上:

  • 不可变的类永远都不应该提供 clone 方法
  • Cloneable 结构与引用可变对象的 final 域的正常做法是不相兼容的。
  • clone 方法是浅拷贝(只拷贝一层),对类所引用的对象需手动拷贝

来看一下浅拷贝和深拷贝的示例:

public class Student implements Cloneable{
    String name;
    int age;
    
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    public Object clone(){
        Object o = null;
        try{
             o = (Student)super.clone();//Object 中的clone()识别出你要复制的是哪一个对象    
        }catch(CloneNotSupportedException e){
             System.out.println(e.toString()); 
        }
        return o; 
    }

    public static void main(String[] args){ 
            Student s1=new Student("zhangsan",18); 
            Student s2=(Student)s1.clone(); 
            System.out.println("克隆后s2:name="+s2.name+","+"age="+s2.age); 
            s2.name="lisi"; 
            s2.age=20; 
            //修改学生2后,不影响学生1的值。
            System.out.println("克隆修改后s1:name="+s1.name+","+"age="+s1.age); 
            System.out.println("克隆修改后s2:name="+s2.name+","+"age="+s2.age);
        }
}

在这里插入图片描述

这时候,若是类的每一个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么被返回的对象则正是所须要的对象,只须要简单地调用super.clone() 而不用作进一步的处理。可是!若是对象中包含其余对象的引用时,那么只是简单的clone就没法作到彻底的克隆了,下面的例子咱们就能够体会到接口

class Professor { 
    String name; 
    int age; 
    Professor(String name,int age){ 
        this.name=name; 
        this.age=age; 
    } 
} 

public class Student implements Cloneable{ 
    String name;// 常量对象。 
    int age; 
    Professor p;// 学生1和学生2的引用值都是同样的。 

    Student(String name,int age,Professor p){ 
        this.name=name; 
        this.age=age; 
        this.p=p; 
    } 

    public Object clone(){ 
        Student o=null; 
        try{ 
                o=(Student)super.clone(); 
        }catch(CloneNotSupportedException e){ 
                System.out.println(e.toString()); 
        } 
        
        return o; 
    } 

    public static void main(String[] args){ 
          Professor p=new Professor("wangwu",50); 
          Student s1=new Student("zhangsan",18,p); 
          Student s2=(Student)s1.clone(); 
          System.out.println("克隆后s1:name="+s1.p.name+","+"age="+s1.p.age);
          System.out.println("克隆后s2:name="+s2.p.name+","+"age="+s2.p.age);
          s2.p.name="lisi"; 
          s2.p.age=30;  
          System.out.println("克隆后s1:name="+s1.p.name+","+"age="+s1.p.age);
          System.out.println("克隆后s2:name="+s2.p.name+","+"age="+s2.p.age);
    } 
}

在这里插入图片描述

从结果上咱们能够看出,s2对s1进行克隆时,对s1的属性Professor p并无进行克隆,致使s1和s2对其引用指向同一个,这会形成s2若改变了值,s1则也被动改变了。那应该如何实现深层次的克隆,即修改s2的教授不会影响s1的教授?其实很简单,只须要对Professor进行修改,以下所示便可get

class Professor  implements Cloneable{ 
            String name; 
            int age; 
            Professor(String name,int age){ 
                this.name=name; 
                this.age=age; 
            } 
    
            public Object clone(){
                Object o = null;
                try{ 
                    o = super.clone(); 
                }catch(CloneNotSupportedException e){ 
                    System.out.println(e.toString()); 
                } 
                return o; 
            }
        }

修改Professor后,还须要在Student的clone方法中加入一句代码:o.p=(Professor)p.clone();

public Object clone(){ 
        Student o=null; 
        try{ 
                o=(Student)super.clone(); 
        }catch(CloneNotSupportedException e){ 
                System.out.println(e.toString()); 
        } 
       o.p=(Professor)p.clone(); 
        return o; 
} 

看到结果就如咱们所但愿的那样。所以,在使用clone时,必定要分清须要克隆的对象属性

4. 如何实现一个行为良好的clone方法

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

4.1 每个域包含的只有基本类型或指向不可变对象的引用

这种情况返回的对象可能满足我们的需要,比如《读书笔记08》中的PhoneNumber类。在此,我们只需声明实现Cloneable接口,然后==对Object中受保护的clone方法提供公有的访问途径==:

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

4.2 域中包含可变对象

如《读书笔记05》中的Stack类。 如果想把该类做成cloneable的,如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例虽然size域具有正确的值(基本类型),但它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。 clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。 递归调用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();
        }
    }

4.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 omitted13}

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

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

// Recursive clone method for class with complex mutable state
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());
        }
    }
    @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();
        }
   }
    ... // Remainder omitted
}

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

4.4 复杂对象的克隆方法

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

5.更好的做法

提供一个拷贝构造器或拷贝工厂来代替 clone 方法 拷贝构造器:

public class MyObject {
  public String field01;
 
  public MyObject() {
  }
 
  public MyObject(MyObject object) {
    this.field01 = object.field01;
  }
}

拷贝静态工厂:

public class MyObject {
  public String field01;
 
  public MyObject() {
  }
 
  public static MyObject newInstance(MyObject object) {
    MyObject myObject = new MyObject();
    myObject.field01 = object.field01;
    return myObject;
  }
}

优点

  • 其不依赖于某一种很有风险的、语言之外的对象创建机制;
  • 其不遵守尚未制定好的文档规范;
  • 其不会与final域的正常使用发生冲突;
  • 其不会抛出不必要的受检查异常;
  • 其不需要类型转换;
  • 采用其代替clone方法时,并没有放弃接口功能特性。

6.总结

如果必须提供clone方法:

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

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

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

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

既然所有的问题都与 Cloneable 接口有关,新的接口就不应该扩展这个接口,新的可扩展的类也不应该实现这个接口。复制功能最好有构造器或工厂提供。(除数组)

7.参考文献

blog.albumenj.cn/archives/12… www.daimajiaoliu.com/daima/47dd7… www.javashuo.com/article/p-h… www.jianshu.com/p/acbc3fb53…

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享