什么是clone
在实际编程过程中,我们常常要遇到这种情况:有一个对象object1,在某一时刻object1中已经包含了一些有效值,此时可能会需要一个和object1完全相同新对象object2,并且此后对object2任何改动都不会影响到object1中的值,也就是说,object1与object2是两个独立的对象,但object2的初始值是由object1对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需 求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法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接口,实现它的目的是作为一个对象的一个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方法的使用。
编写一个被克隆对象Student类:
package com.kevin.clone; /** * 创建一个简单实例演示clone方法 * @author Kevin * */ public class Student implements Cloneable { private String name; private String gender; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } @Override protected Student clone() throws CloneNotSupportedException { return (Student)super.clone(); } @Override public String toString() { return " [name=" + name + ", gender=" + gender + "]"; } }
编写测试代码:
package com.kevin.clone; /** * 测试clone方法 * @author Kevin * */ public class test_clone { public static void main(String[] args){ Student student1 = new Student(); student1.setName("Kevin"); student1.setGender("Male"); System.out.println("student1"+student1); try{ Student student2 = student1.clone(); System.out.println("Clone student2 from student1..."); System.out.println("student2"+student2);
System.out.println(student1.equals(student2)); System.out.println("Alter student2..."); student2.setName("Alice"); student2.setGender("Female"); System.out.println("student1"+student1); System.out.println("student2"+student2); }catch(CloneNotSupportedException e){ e.printStackTrace(); } } }
输出结果:
student1 [name=Kevin, gender=Male] Clone student2 from student1... student2 [name=Kevin, gender=Male]
false Alter student2... student1 [name=Kevin, gender=Male] student2 [name=Alice, gender=Female]
分析:
● 一是希望能实现clone功能的CloneClass类实现了Cloneable接口,这个接口属于java.lang包, java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。
● 二是重载了clone()方法。最 后在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或间接调 用了java.lang.Object类的clone()方法。下面再详细的解释一下这几点。
● 最后仔细观察一下Object类的clone()一个native方法,native方法的效率一般来说都是远高于java中的非 native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了 clone功能。对于第二点,也要观察Object类中的clone()还是一个protected属性的方法。这也意味着如果要应用clone()方 法,必须继承Object类,在Java中所有的类是缺省继承Object类的,也就不用关心这点了。然后重载clone()方法。还有一点要考虑的是为 了让其它类能调用这个clone类的clone()方法,重载之后要把clone()方法的属性设置为public。
那么clone类为什么还要实现Cloneable接口呢?需要注意的是,Cloneable接口是不包含任何方法的,其实这个接口仅仅是一个标志,而且 这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方 法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。
影子克隆和深度克隆
下面通过一个实例来演示什么是影子克隆。
编写一个Teacher类(其中包含一个Course属性):
package com.kevin.clone; /** * 测试影子克隆方法 * @author Kevin * */ public class Teacher implements Cloneable{ private String name; private Integer age; private Course course; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Course getCourse() { return course; } public void setCourse(Course course) { this.course = course; } @Override protected Teacher clone() throws CloneNotSupportedException { return (Teacher) super.clone(); } @Override public String toString() { return "Teacher [name=" + name + ", age=" + age + ", course=" + course + "]"; } }
package com.kevin.clone; public class Course { private String name; private Integer id; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Override public String toString() { return "Course [name=" + name + ", id=" + id + "]"; } }
编写一个测试类:
package com.kevin.clone; public class Course { private String name; private Integer id; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Override public String toString() { return "Course [name=" + name + ", id=" + id + "]"; } }
输出结果如下:
teacher1 [name=Kevin, age=22, course=Course [name=Math, id=66]] Clone teacher2 from teacher1... teacher2 [name=Kevin, age=22, course=Course [name=Math, id=66]] Alter teacher2... teacher1 [name=Kevin, age=22, course=Course [name=English, id=88]] teacher2 [name=Ryan, age=18, course=Course [name=English, id=88]]
通过分析结果可知,当我们修改克隆对象teacher2的时候,teacher1的course属性也被修改了,如果通过查看内存地址的形式我们可以发现,他们两个course其实是同一个对象。由此我们可以推断,调用clone方法产生的效果是:现在内存中开辟一块和原始对象一样的空间,然后拷贝原始对象中的内容。但是对于基本数据类型,这样的操作是没有问题的,但对于非基本类型,它们保存的仅仅是对象的引用,这就是为什么clone后的非基本类型变量和原始对象中相应的变量会指向的是同一个对象。这就是所谓的影子克隆。
为了解决影子克隆所产生的问题,我们就需要使用深度克隆方案。通过对以上实例改进后的方案如下:
package com.kevin.clone; /** * 测试影子克隆方法 * @author Kevin * */ public class Teacher implements Cloneable{ private String name; private Integer age; private Course course; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Course getCourse() { return course; } public void setCourse(Course course) { this.course = course; } @Override protected Teacher clone() throws CloneNotSupportedException { Teacher teacher = (Teacher)super.clone(); teacher.course = course.clone(); return teacher; } @Override public String toString() { return " [name=" + name + ", age=" + age + ", course=" + course + "]"; } }
package com.kevin.clone; public class Course implements Cloneable{ private String name; private Integer id; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } protected Course clone() throws CloneNotSupportedException{ return (Course)super.clone(); } @Override public String toString() { return "Course [name=" + name + ", id=" + id + "]"; } }
同样使用原来的测试类:
package com.kevin.clone; /** * 测试clone方法 * @author Kevin * */ public class test_clone2 { public static void main(String[] args){ Teacher t1 = new Teacher(); t1.setName("Kevin"); t1.setAge(22); Course c1 = new Course(); c1.setName("Math"); c1.setId(66); t1.setCourse(c1); System.out.println("teacher1"+t1); try{ Teacher t2 = t1.clone(); System.out.println("Clone teacher2 from teacher1..."); System.out.println("teacher2"+t2); System.out.println("Alter teacher2..."); t2.setName("Ryan"); t2.setAge(18); //修改courese属性 t2.getCourse().setName("English"); t2.getCourse().setId(88); System.out.println("teacher1"+t1); System.out.println("teacher2"+t2); }catch(CloneNotSupportedException e){ e.printStackTrace(); } } }
输出结果:
teacher1 [name=Kevin, age=22, course=Course [name=Math, id=66]] Clone teacher2 from teacher1... teacher2 [name=Kevin, age=22, course=Course [name=Math, id=66]] Alter teacher2... teacher1 [name=Kevin, age=22, course=Course [name=Math, id=66]] teacher2 [name=Ryan, age=18, course=Course [name=English, id=88]]
分析:由以上运行结果可知,进行过深度克隆之后,对clone产生的teacher2对象的course属性进行修改时,并未影响到原对象teacher1的course属性。
任何类都可以实现深度clone吗
答案是否定的,例如,StringBuffer,看一下 JDK API中关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer还是一个 final类,这也是说我们也不能用继承的办法间接实现StringBuffer的clone。如果一个类中包含有StringBuffer类型对象或和 StringBuffer相似类的对象,我们有两种选择:要么只能实现影子clone,要么自己重新生成对象: new StringBuffer(oldValue.toString()); 进行赋值。
要知道除了基本数据类型(byte,short,int,long,double等)可自动实现深度克隆以外,其它例如Integer、String、Double等是一特殊情况。
下面通过一个实例来演示上述结论。
package com.kevin.clone; public class Book implements Cloneable{ public String name; public StringBuffer author; protected Book clone() throws CloneNotSupportedException{ return (Book)super.clone(); } }
测试代码:
package com.kevin.clone; /** * 测试clone方法 * @author Kevin * */ public class test_clone3 { public static void main(String[] args){ Book book = new Book(); book.name = new String("Think in Java"); book.author = new StringBuffer("Kevin"); System.out.println("Before clone book.name :"+book.name); System.out.println("Before clone book.author :"+book.author); Book book_clone = null; try{ book_clone = (Book)book.clone(); }catch(CloneNotSupportedException e){ e.printStackTrace(); } book_clone.name = book_clone.name.substring(0,5); book_clone.author = book_clone.author.append(" Zhang"); System.out.println("\nAfter clone book.name :"+book.name); System.out.println("After clone book.author :"+book.author); System.out.println("\nAfter clone book_clone.name :"+book_clone.name); System.out.println("After clone book_clone.author :"+book_clone.author); } }
输出结果:
Before clone book.name :Think in Java
Before clone book.author :Kevin
After clone book.name :Think in Java
After clone book.author :Kevin Zhang
After clone book_clone.name :Think
After clone book_clone.author :Kevin Zhang
分析:有上述结果可知,String类型的变量看起来好像实现了深度clone,因为对book_clone.name的改动并没有影响到book.name。实质上,在clone的时候book_clone.name与book.name仍然是引用,而且都指向了同一个 String对象。但在执行book_clone.name = book_clone.name.substring(0,5)的时候,生成了一个新的String类型,然后又赋回给book_clone.name。这是因为String被 Sun公司的工程师写成了一个不可更改的类(immutable class),在所有String类中的函数都不能更改自身的值。类似的,String类中的其它方法也是如此,都是生成一个新的对象返回。当然StringBuffer还是原来的对象。
需要知道的是在Java中所有的基本数据类型都有一个相对应的类,例如Integer类对应int类型,Double类对应double类型等等,这些类也 与String类相同,都是不可以改变的类。也就是说,这些的类中的所有方法都是不能改变其自身的值的。这也让我们在编clone类的时候有了一个更多的 选择。同时我们也可以把自己的类编成不可更改的类。