NotOnlyJava

http://www.ibm.com/developerworks/cn/java/j-lo-serial/
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

深入分析java的clone

Posted on 2013-04-19 16:20  NotOnlyJava  阅读(802)  评论(0编辑  收藏  举报

 

1 什么是clone?

 1.1广义的clone

广义的克隆可以理解为复制、拷贝和翻倍,就是从原型中产生出同样的复制品,它的外表及遗传基因与原型完全相同。在程序设计中clone的定义是这样的:指对一个对象制造一个精确的复本。实际上在OOP的编程中,clone()方法来自著名的设计模式——原型模式(Prototype),该模式常用于创建复杂的或者耗时的实例,因为这些对象的内存的分配和成员变量的初始化较为耗时,而复制一个已经存在的实例使程序运行更高效。

1.2 java中的clone

相比C++,java是一种更纯洁的OOP语言,因此在java中,我们能更容易的使用各种模式设计出健壮、易于扩展的程序。JDK也提供了许多设计模式的良好实现,在Object类中clone()实现了原型模式,源码如下:

protected native Object clone() throws CloneNotSupportedException;

该方法是一个受保护的本地方法,我们都知道protected方法只能在子类里调用,但是在java中,每个类都是Object的子类,所以可以在任何代码调用clone(),如下代码:

 

 

public class TestClone {

  public static void main(String[] args) throws CloneNotSupportedException {

      TestClone t = new TestClone();

      t.clone();

  }

}

这个main方法一运行就抛出异常:

Exception in thread "main" java.lang.CloneNotSupportedException: com.czp.TestClone

        at java.lang.Object.clone(Native Method)

  at com.czp.TestClone.main(TestClone.java:7)

原因是Object的clone方法默认抛出受检查异常CloneNotSupportedException,要想自定义的类具有clone特性,需要实现Cloneable接口,这是一个标志接口,表示该类的对象具有clone功能,让我们修改之前的代码:

public class TestClone implements Cloneable{

 

   public static void main(String[] args) throws CloneNotSupportedException {

      TestClone t = new TestClone();

      TestClone copy = (TestClone) t.clone();

      System.out.println(t);

      System.out.println(copy);

   }

}

程序正常运行,并输出结果如下:

com.czp.TestClone@148aa23

com.czp.TestClone@199f91c

 

TestClone的clone调用Object的clone方法,jdk的doc是这样描述该方法的:

创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。这样做的目的是,对于任何对象 x,表达式:

x.clone() != x为 true,表达式: x.clone().getClass() == x.getClass()也为 true,但这些并非必须要满足的要求。一般情况下: x.clone().equals(x)为 true,但这并非必须要满足的要求。

按照惯例,返回的对象应该通过调用 super.clone 获得。如果一个类及其所有的超类(Object 除外)都遵守此约定,则 x.clone().getClass() == x.getClass()。

按照惯例,此方法返回的对象应该独立于该对象(正被复制的对象)。要获得此独立性,在 super.clone 返回对象之前,有必要对该对象的一个或多个字段进行修改。这通常意味着要复制包含正在被复制对象的内部“深层结构”的所有可变对象,并使用对副本的引用替换对这些对象的引用。如果一个类只包含基本字段或对不变对象的引用,那么通常不需要修改 super.clone 返回的对象中的字段。

Object 类的 clone 方法执行特定的复制操作。首先,如果此对象的类不能实现接口 Cloneable,则会抛出 CloneNotSupportedException。注意,所有的数组都被视为实现接口 Cloneable。否则,此方法会创建此对象的类的一个新实例,并像通过分配那样,严格使用此对象相应字段的内容初始化该对象的所有字段;这些字段的内容没有被自我复制。所以,此方法执行的是该对象的“浅表复制”,而不“深层复制”操作。

Object 类本身不实现接口 Cloneable,所以在类为 Object 的对象上调用 clone 方法将会导致在运行时抛出异常。

简言之, Object的clone方法返回一个新对象,该对象与源对象具有相同的成员变量,实现代码与下面的类似:

public class TestClone implements Cloneable{

  private int id;

  private String name=”test”;

  public int getId() {

      return id;

  }

public void setId(int id) {

      this.id = id;

  }

  public String getName() {

      return name;

  }

  public void setName(String name) {

      this.name = name;

  }

  public Object clone()throws CloneNotSupportedException{

      TestClone copy = new TestClone();

      copy.setId(id);

      copy.setName(name);

      return copy;

  }

public static void main(String[] args) throws CloneNotSupportedException {

      TestClone test = new TestClone();

      TestClone copy = (TestClone) test.clone();

      System.out.println(test);

      System.out.println(copy);

  }

这就是典型的浅克隆,此时test和copy在内存中大致是这样:

 

   copy和test是两个独立的对象,占用不同的内存空间,修改彼此的成员变量不会对另一方造成影响,实际上Object的clone()也是如此,对于基本类型byte、char、short、int、long、float、double会进行值拷贝,而对引用类型和数组,则是地址拷贝(也叫引用拷贝),就是指在新的对象中声明一个引用,该引用的地址指向被拷贝对象中的对应的成员的地址,如上图的copy的name和test的name指向同一个String对象的地址。此时,修改copy的name属性不会影响test的属性,如下调用:

public static void main(String[] args) throws CloneNotSupportedException {

      TestClone test = new TestClone();

      TestClone copy = (TestClone) test.clone();

      copy.setName("change from clone");

      System.out.println("test name:"+test.getName());

      System.out.println("copy name:"+copy.getName());

  }

输出如下:

test name:test

copy name:change from clone

代码copy.setName("change from clone")只是把copy的name指向另一个String对象,所以对象test没有影响,如图:


似乎一切都很美好,没有任何疑虑,让我们再来看看这幅图

 

   copy和test的name指向同一个String对象,如果我们从copy中拿到name的引用,将name的值修改为”change from clone”,那么test的name值也会改为”change from clone”,但是这是无法实现的,java中String对象是不可变的,一旦创建就不能修改它的值,由此可以得出一个结论:

如果一个类的成员变量只有基本类型和String类型,调用Object的clone()可以获取一个安全的拷贝。

但是如果成员变量是一个可变的,那将会造成不可预知的bug,

public class TestClone implements Cloneable{

    private int id;

    private Date date;

    private TestClone() {

       date = new Date();

    }

    public int getId() {

       return id;

    }

    public void setId(int id) {

       this.id = id;

    }

    public Date getDate() {

       return date;

    }

    public void setDate(Date date) {

       this.date = date;

    }

    public Object clone()throws CloneNotSupportedException{

       TestClone copy = new TestClone();

       copy.setId(id);

       copy.setDate(date);

       return copy;

    }

    @SuppressWarnings("deprecation")

    public static void main(String[] args) throws CloneNotSupportedException {

       TestClone test = new TestClone();

       //修改前 当前时间2013-4-19 14:25:41

       System.out.println("test date:"+test.getDate().toLocaleString());

       TestClone copy = (TestClone) test.clone();

       Date date = copy.getDate();

       date.setHours(3);

       //修改后

    System.out.println("testdate:"+test.getDate().toLocaleString());

    }

}

输出如下:

test date:2013-4-19 14:25:41

test date:2013-4-19 3:25:41

我们原本只想修改copy对象的date属性,

TestClone copy = (TestClone) test.clone();

Date date = copy.getDate();

date.setHours(3);

但是,test的对象的date也被修改了,这就违背了clone方法的初衷,clone方法本来是想返回一个独立的新对象,但现在看来两个对象并不是真正的独立。假释我们从一个公共的模块中获取一个共享对象,并调用clone()获取一个拷贝,然后我们心安理得的修改了其中某个引用类型成员的值,这时其他模块再拿到这个对象使用就可能出现某个判断无法进去甚至某段直接抛异常,这种bug往往很难定位。由此我们得出两个结论:

 1 如果一个类中有可变对象成员,那么通过默认clone()获取的对象是不安全的。

2 如果类是共用的,并且该中包含可变对象成员,那么除非必要,就不要实现Cloneable接口,这样调用者就无法clone()该对象。

要想解决这个问题,有两种方法:

1.把类的成员变量都设计成像String对象的不可变。

2.重写clone方法实现深拷贝。

第一种方法有现成的代码可以参考,无须赘述,第二种方法,我们需要调用引用类型成员变量的clone方法,示例如下:

public class TestClone implements Cloneable{

 

    private int id;

    private Date date;

   

    private TestClone() {

       date = new Date();

    }

    public int getId() {

       return id;

    }

    public void setId(int id) {

       this.id = id;

    }

    public Date getDate() {

       return date;

    }

    public void setDate(Date date) {

       this.date = date;

    }

   

    @Override

    public TestClone clone() throws CloneNotSupportedException {

       TestClone copy = (TestClone) super.clone();

       copy.setDate((Date) date.clone());

       return copy;

    }

    @SuppressWarnings("deprecation")

    public static void main(String[] args) throws CloneNotSupportedException {

       TestClone test = new TestClone();

       //修改前 当前时间2013-4-19 14:25:41

    System.out.println("testdate:"+test.getDate().toLocaleString());

       TestClone copy = (TestClone) test.clone();

       Date date = copy.getDate();

       date.setHours(3);

    //修改后

    System.out.println("testdate:"+test.getDate().toLocaleString());

    }

}

这样,修改copy的date属性的值也不会影响test的date,上面的程序输出如下:

test date:2013-4-19 15:22:07

test date:2013-4-19 15:22:07

当然,这段代码的正确运行是依赖于Date重写了clone方法,所以要想真正的实现深度克隆,必须要保证类的成员变量也重写了clone方法,但是这点往往很难得到保证。有人提出用序列化的方式进行深度copy,代码如下:

public Object clone() throws IOException,OptionalDataException,ClassNotFoundException,

{

 //将对象写到流里

 ByteArrayOutoutStream bo=new ByteArrayOutputStream();

 ObjectOutputStream oo=new ObjectOutputStream(bo);

 oo.writeObject(this);

 //从流里读出来

 ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());

 ObjectInputStream oi=new ObjectInputStream(bi);

 return(oi.readObject());

}

这种方法的确能copy出独立的安全的对象,但是用到耗时的I/O操作,典型的为了clone而clone,完全违背了clone遵循的原型模式的精神——省时、高效。如果复制一个对象变的如此耗时和低效,那为什么不直接创建一个新的对象呢?

总之,少用甚至不用clone(),是解决clone()缺陷的最好方法。