对象克隆 ( object clone ) 是指创建对象的完整副本。它创建了当前对象的类的新实例,并使用当前对象的相应字段的内容初始化新实例的所有字段。
使用赋值运算符创建引用变量的副本
Java 语言中,不存在什么运算符可以用来创建对象的副本。
与 C++ 不同的是,Java 中的赋值运算符只会创建引用变量的副本,而不是对象的副本。
我们写一小段程序来演示下这一点
JavaTester.java
import java.io.*; class JavaTester { int x, y; JavaTester() { x = 10; y = 20; } public static void main(String[] args) { JavaTester ob1 = new JavaTester(); System.out.println(ob1.x + " " + ob1.y); // 赋值运算符创建了对象的引用 // 也就是说 obj1 和 obj2 指向同一个对象 JavaTester ob2 = ob1; // 任何对 obj2 对象的变更都会反映到 obj1 上 ob2.x = 100; System.out.println(ob1.x+" "+ob1.y); System.out.println(ob2.x+" "+ob2.y); } }
编译运行上面的代码,输出结果如下
[yufei@www.twle.cn java]$ javac JavaTester.java && java JavaTester 10 20 100 20 100 20
使用 clone 方法创建对象的副本
要创建一个对象的完整副本,那么该对象所属的类或者类的父类必须存在一个 clone()
方法。
clone()
方法的实现有几个要求
- 每个实现
clone()
的类都应该调用super.clone()
来获取克隆的对象引用。 - 实现了
clone()
方法的类还必须实现java.lang.Cloneable
接口,否则当在该类的对象上调用clone()
方法时,它将抛出CloneNotSupportedException
clone()
方法的原型如下
protected Object clone() throws CloneNotSupportedException
使用 clone() 方法实现浅拷贝
我们先来看一段代码,该代码使用 clone()
方法实现了浅拷贝
import java.util.ArrayList; class Test { int x, y; } class Test2 implements Cloneable { int a; int b; Test c = new Test(); public Object clone() throws CloneNotSupportedException { return super.clone(); } } // Driver class public class JavaTester { public static void main(String args[]) throws CloneNotSupportedException { Test2 t1 = new Test2(); t1.a = 10; t1.b = 20; t1.c.x = 30; t1.c.y = 40; Test2 t2 = (Test2)t1.clone(); t2.a = 100; t2.c.x = 300; System.out.println(t1.a + " " + t1.b + " " + t1.c.x + " " + t1.c.y); System.out.println(t2.a + " " + t2.b + " " + t2.c.x + " " + t2.c.y); } }
编译运行上面的代码,输出结果如下
[yufei@www.twle.cn java]$ javac JavaTester.java && java JavaTester 10 20 300 40 100 20 300 40
上面这段代码中,t1.clone()
方法返回对象 t1
的浅拷贝,因为 t2
对象还是指向了原来的那个对象。
如果要实现一个深拷贝,则应该在 clone()
方法中对对象副本进行一些修改
深拷贝 vs 浅拷贝
浅拷贝是复制对象的方法,默认情况下是克隆。在该方法中,旧对象 X
的字段被复制到新对象 Y
。在复制对象类型字段时,引用被复制到 Y
,即对象 Y
将指向与 X
指向相同的位置。如果字段值是基本类型,则复制基本类型的值。
因此,在浅拷贝中,对象 X 或 Y 中引用对象所做的任何更改都将反映在其他对象中。
使用 clone() 方法实现深拷贝
如果我们创建了对象 X
的一个深拷贝副本并将它赋值给对象 Y
,那么 Y
对象中的所有字段都是全新的。这意味着所有对对象 X
的变更或对对象 Y
的变更都不会互相影响到对方。
深拷贝会赋值所有的字段,也就是给所有的字段开辟一个全新的存储空间。
我们来写一个小范例,演示下如何创建深拷贝
JavaTester.java
import java.util.ArrayList; class Test { int x, y; } class Test2 implements Cloneable { int a; int b; Test c = new Test(); public Object clone() throws CloneNotSupportedException { Test2 t = (Test2)super.clone(); t.c = new Test(); // 返回一个新的对象,因此会创建一个深拷贝 return t; } } // Driver class public class JavaTester { public static void main(String args[]) throws CloneNotSupportedException { Test2 t1 = new Test2(); t1.a = 10; t1.b = 20; t1.c.x = 30; t1.c.y = 40; Test2 t2 = (Test2)t1.clone(); t2.a = 100; t2.c.x = 300; System.out.println(t1.a + " " + t1.b + " " + t1.c.x + " " + t1.c.y); System.out.println(t2.a + " " + t2.b + " " + t2.c.x + " " + t2.c.y); } }
编译运行以上代码,输出结果如下
[yufei@www.twle.cn java]$ javac JavaTester.java && java JavaTester 10 20 30 40 100 20 300 0
上面的代码中,我们将 super.clone()
方法返回的 Test2
对象赋值给 t
。到这一步,其实和浅拷贝没什么大的区别。
重要的区别在于吓一条语句 t.c = new Test()
。这条语句完成了 c
字段的深拷贝。
因为是深拷贝,因此对对象 t3
的修改并不会影响到 t1
。
clone() 方法的优点
-
如果我们使用赋值运算符将对象引用分配给另一个引用变量,那么它将指向旧对象的相同地址位置,并且不会创建该对象的新副本。因此,引用变量的任何变化都将反映在原始对象中
-
如果我们使用复制构造函数,那么我们必须显式复制所有数据,即我们必须在构造函数中明确重新分配类的所有字段。
-
但是在
clone()
方法中,创建新副本的工作是由方法本身完成的。为了避免额外的处理,我们使用对象克隆。
后记
clone()
方法可以这么理解
-
从
java.lang.Object
中继承而来的clone()
方法实现的是浅拷贝,因为它并不会对引用类型的字段进行深拷贝 -
如果要实现深拷贝,则需要自己重写
clone()
方法,为每一个引用类型的字段创建一个全新的引用