Java中Object类之clone()方法详解

一、clone()方法

       clone意思是克隆、复制。在Java语言中,当对象调用clone()方法时,就会复制已有的对象。clone()方法在根类Object中定义如下。

/**
 * Class  Object is the root of the class hierarchy. Every class has  Object as a superclass. 
 * All objects, including arrays, implement the methods of this class.
*/
public class Object {
    /**
     * Creates and returns a copy of this object.  The precise meaning of "copy" may depend on the class of the object. 
     * The general intent is that, for any object {@code x}, the expression: x.clone() != x will be true,
     * and that the expression: x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
     * While it is typically the case that: x.clone().equals(x) will be  true, this is not an absolute requirement.
    */
    protected native Object clone() throws CloneNotSupportedException;
}

         从上面对clone方法的注解可知clone方法的通用约定:对于任意一个对象x,表达式①x.clone != x将会是true;表达式②x.clone().getClass()==x.getClass()将会是true,但不是绝对的。通常情况下,表达式③x.clone().equals(x)将会是true,但是这也不是绝对的。
        从源代码可知,根类Object的clone方法是用protected关键字修饰,这样做是为避免我们创建每一个类都默认具有克隆能力。这样做造成的后果就是:对于那些简单使用一下这个类的客户程序员来说,他们不会默认使用这个方法;其次,我们不能利用指向基础类的一个句柄来调用clone方法。在编译期间,实际上是通知我们对象不可克隆的一种方式。

二、Cloneable接口        

        要使类具有克隆能力能力时,需要实现Cloneable接口,实现它的目的是作为一个对象的一个mixin(混入)接口,表明这个对象是允许克隆的。它的源码如下:

/**
 * A class implements the Cloneable interface to indicate to the {@link java.lang.Object#clone()} method that 
 * it is legal for that method to make a field-for-field copy of instances of that class.
*/
public interface Cloneable {
}

         可以看出Cloneable是一个空接口(标记接口),它决定了Object中受保护的clone方法的实现行为:如果一个类实现了Cloneable接口,Object的clone方法就返回这个对象的逐域拷贝,否则就抛出CloneNotSupportedException异常。如果实现了这个接口,类和它所有的超类都无需调用构造器就可以创建对象。下面是一个简单的clone方法应用。

public class Person implements Cloneable {
	private String name;
	private Integer age;
	private String sex;

	public Person() {
		super();
	}

	public Person(String name, Integer age, String sex) {
		super();
		this.name = name;
		this.age = age;
		this.sex = sex;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public void setSex(String sex) {
		this.sex = sex;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]";
	}

	public Person clone() throws CloneNotSupportedException {
		return (Person) super.clone();
	}

	public static void main(String[] args) {
		Person person = new Person("Jachen", 23, "boy");
		System.out.println("person:" + person);
		try {
			Person person2 = person.clone();
			System.out.println("person2:" + person2);
			System.out.println("person.equals(person2):"+ person2.equals(person));
			person2.setName("Anna");
			person2.setSex("girl");
			person2.setAge(22);
			System.out.println(person2.getClass() == person.getClass());
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
	}
}

运行结果如下图所示:

        由运行结果可知,当person对象调用clone方法后复制了一个跟它“相似”的对象,可是调用equals方法后,得知他们在堆中的地址是不同的(JVM中堆、栈简介),内存图结构如下:

                                             

三、Object.clone()的运行效果

        根类Object中的clone()方法负责建立正确的存储容量,并通过“按位复制”将所有二进制从原始对象中复制到新对象的存储空间。也就是说,它并不只是预留存储空间以及复制一个对象--实际需要调查出欲复制的新对象准确大小,然后再复制那个对象。由于这些工作都是由根类定义的clone()方法内部代码进行的,这个过程需要用RTTI(运行时类型鉴定)来判断欲克隆对象的实际大小。采用这种方式,clone()方法便可建立起正确数量的存储空间,并对那个类型进行正确的按位复制。

        克隆过程的第一个部分通常都应该是调用super.clone()。通过进行一次准确的复制,这样做可为后续的克隆进程建立起一个良好的基础。随后,可采取另一些必要的操作,以完成最终的克隆。

        通常可在从一个能克隆的类里调用 super.clone(),以确保所有基础类行动(包括 Object.clone())能够进行。随着是为对象内每个句柄都明确调用一个 clone();否则那些句柄会别名变成原始对象的句柄。构建器的调用也大致相同——首先构造基础类,然后是下一个衍生的构建器……以此类推,直到位于最深层的衍生构建器。区别在于 clone()并不是个构建器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。

        若新建一个类,它的基础类会默认为Object,并默认为不具备克隆能力。只要不明确地添加克隆能力,这种能力便不会自动产生。但我们可以在任何层添加它,然后便可从那个层开始向下具有克隆能力。

四、深克隆

          上面讲的是“浅复制”,接下来我们来讲解“深复制”。如果对象中包含的域引用了可变的对象,使用上述这种简单的clone实现(super.clone())可能会导致灾难性的后果,如下面代码所示。

import java.util.Arrays;
import java.util.EmptyStackException;
//栈类定义
public class Stack implements Cloneable{

	private Object[] elements;//用数组来存储栈的元素
	private int size = 0;//栈中元素的个数
	private static final int DEFAULT_INITIAL_CAPACITY = 16;//默认初始化大小为16
	
	public Stack(){
		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);
		}
	}
	//克隆栈对象
	public Stack clone () throws CloneNotSupportedException{
		return (Stack) super.clone();
	}
	
	public static void main(String[] args) throws CloneNotSupportedException {
		Stack stack = new Stack();
		stack.push("Jachen");
		stack.push("Anna");
		stack.push("Jack");
		Stack cloneStack = stack.clone();
		cloneStack.pop();
		
		for(int i = 0;i<stack.size;i++){
			System.out.print(stack.elements[i] + ",");
		}
	}
}

当clone方法仅仅返回super.clone();得到这样的Stack实例:其size域具有正确的值,但是它的elements域将引用于原始Stack实例相同的数组。上面程序运行结果如下:

其内存中的情况如下图所示:

                                        

        如果调用Stack类中唯一的构造器,这种情况就永远不会发生。实际上,clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。为了使Stack类中clone方法正常工作,它必须要拷贝栈内部的信息,clone方法中代码修改如下:

public Stack clone() throws CloneNotSupportedException{
    Stack stack = (Stack)super.clone();
    stack.elements = elements.clone();
    return stack;
}

五、必要时进行保护性拷贝

        在Java安全的语言中,如果不采取一些措施,还是无法保证与其他类隔离开来。假设类的客户端尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。例如下面的程序可能会受到破坏。

 

import java.util.Date;

public  class Period {

	private final Date start;
	private final Date end;
	
	public Period(Date start,Date end){
		if(start.compareTo(end) > 0){
			throw new IllegalArgumentException();
		}
		this.start = start;
		this.end = end;
	}

	public Date getStart() {
		return start;
	}

	public Date getEnd() {
		return end;
	}
	public static void main(String[] args) {
		Date start = new Date();
		Date end = new Date();
		Period period = new Period(start, end);
		end.setYear(2017);
	}
}

上面的代码会使Period实例内部信息遭受到攻击,需要对构造器的每个可变参数进行保护性拷贝。

public Period(Date start,Date end){
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
    if(this.start.compareTo(this.end) > 0){
        throw new IllegalArgumentException();
	}
}

保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。

        每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,有必要考虑下客户提供的对象是否有可能是可变的。如果是,就考虑类是否能容忍这种变化,如果答案是否定的,则由必要进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入数据结构中。

        在内部组件被返回给客户端之前,不管类是否为不可变,在把一个指向内部可变组件的引用返回给客户端之前,应该返回保护性拷贝。长度非零的数组总是可变的。因此,在把内部数组返回给客户端之前,应该总是要进行保护性拷贝。

        简而言之,如果类是不可变的,一般不需要进行保护性拷贝。若类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝成本受到限制,并且类信任客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。

        总之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone方法。此方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。如果该类只包含基本类型的域,或者指向不可变对象的引用,那多半的情况下是没有域需要修正的。

        其实实现Cloneable接口具有很多问题,很多接口都不应该扩展这个接口,也不应该实现这个接口。因此很多程序员从来不去覆盖clone方法,也从来不去调用它,除非拷贝数组。所以,慎重选择覆盖clone()方法。

 

不可变类和JVM内存模型相应文章:不可变类总结JVM内存模型

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值