Java中clone方法的使用
1. 什么是clone
在实际编程过程中,我们常常要遇到这种情况:有一个对象object1,在某一时刻object1中已经包含了一些有效值,此时可能会需要一个和object1完全相同新对象object2,并且此后对object2任何改动都不会影响到object1中的值。也就是说,object1与object2是两个独立的对象,但object2的初始值是由object1对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的。
@Test
public void testassign() {
Person p1=new Person();
p1.setAge(31);
p1.setName("Peter");
Person p2=p1;
System.out.println(p1==p2); // 输出为 true
}
2.如何使用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;
}
要使类具有克隆能力时,需要实现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是一个空接口(标记接口),对Cloneable接口和Object的clone()方法说明:
- Cloneable接口
(1)此类实现了Cloneable接口,以指示Object的clone()方法可以合法地对该类实例进行按字段复制;
(2)如果在没有实现Cloneable接口的实例上调用Object的clone()方法,则会导致抛出 CloneNotSupporteddException;
(3)按照惯例,实现此接口的类应该使用公共方法(public)重写Object的clone()方法,Object的clone()方法是一 个受保护(protected)的方法; - Object的clone()方法:创建并返回此对象的一个副本。对于任何对象x,表达式:
(1)x.clone() != x为true
(2)x.clone().getClass() == x.getClass()为true
(3)x.clone().equals(x)一般情况下为true,但这并不是必须要满足的要求
因此,总结如下:Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,所以实体类使用克隆的前提是:
① 实现Cloneable接口,这是一个标记接口,自身没有方法;
② 覆盖clone()方法,可见性提升为public。
编写一个被克隆对象Student类:
package test;
/**
* 创建一个简单实例演示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 test;
/**
* 测试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(student2==student1);
System.out.println(student2.getClass() == student1.getClass());
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();
}
}
}
输出结果:
分析:
● 一是希望能实现clone功能的CloneClass类实现了Cloneable接口,这个接口属于java.lang包, java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。
● 二是在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或间接调用了java.lang.Object类的clone()方法。
● 最后是重载了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异常(如上Cloneable接口所述)。
3. 影子克隆(浅复制/拷贝)和深度克隆(深复制/拷贝)
在Java中,Clone方法有两种不同的模式,即浅复制和深复制(也被称为浅拷贝和深拷贝)。对于浅复制,只是对象的引用得到的复制;如果对象中存在其他对象的引用,使用浅复制后,源对象和复制后的对象中对其他对象的引用会指向同一个内存地址。如果要完全把两个对象在内存中分开,必须使用深复制。
- 浅拷贝:被复制对象的所有变量都含有与原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅拷贝所考虑的对象,而不拷贝它引用的对象。
- 深拷贝:被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把复制的对象所引用的对象都复制了一遍。
假如定义如下一个类:
class Test{
private int i;
private Date s=new Date;
}
下图展示深拷贝和浅拷贝的区别
编写一个Teacher类(其中包含一个Course属性):
package test;
/**
* 测试影子克隆方法
* @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 test;
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 test;
public class test_clone1 {
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();
}
}
}
输出结果:
通过分析结果可知,当我们修改克隆对象teacher2的时候,teacher1的course属性也被修改了,如果通过查看内存地址的形式我们可以发现,他们两个course其实是同一个对象。由此我们可以推断,调用clone方法产生的效果是:现在内存中开辟一块和原始对象一样的空间,然后拷贝原始对象中的内容。对于基本数据类型,这样的操作是没有问题的,但对于非基本类型,它们保存的仅仅是对象的引用,这就是为什么clone后的非基本类型变量和原始对象中相应的变量会指向的是同一个对象。这就是所谓的浅拷贝。
为了解决影子克隆所产生的问题,我们就需要使用深度克隆方案。通过对以上实例改进后的方案如下:
Teacher类中:
@Override
protected Teacher clone() throws CloneNotSupportedException {
Teacher teacher = (Teacher)super.clone();
teacher.course = course.clone();
return teacher;
}
Course类也要实现Cloneable()接口:
package test;
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;
}
@Override
protected Course clone() throws CloneNotSupportedException{
return (Course)super.clone();
}
@Override
public String toString() {
return "Course [name=" + name + ", id=" + id + "]";
}
}
同样使用原来的测试类,输出结果如下:
分析:由以上运行结果可知,进行过深度克隆之后,对clone产生的teacher2对象的course属性进行修改时,并未影响到原对象teacher1的course属性。
4. 任何类都可以实现深度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 test;
public class Book implements Cloneable{
public String name;
public StringBuffer author;
protected Book clone() throws CloneNotSupportedException{
return (Book)super.clone();
}
}
package test;
/**
* 测试clone方法
* @author Kevin
*
*/
public class test_clone2 {
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);
}
}
输出结果如下:
分析:有上述结果可知,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类的时候有了一个更多的 选择。同时我们也可以把自己的类编成不可更改的类。