TA的每日心情 | 开心 2021-3-12 23:18 |
---|
签到天数: 2 天 [LV.1]初来乍到
|
假设在你的应用中使用一些对象,你如何拷贝你的对象呢?最明显的方法是将一个对象简单的赋值给另一个,就像这样:
obj2 = obj1;
但是这个方法实际上没有拷贝对象而仅仅是拷贝了一个对象引用,换言之,在你执行这个操作后仍然只有一个对象,但是多出了一个对该对象的引用。
如果这个看似明显的方法不能正常工作,那么如何实际的拷贝一个对象呢?为什么不试试Object.clone呢?
这个方法对Object的所有子类都是可用的。例如:
class A {
private int x;
public A(int i) {
x = i;
}
}
public class CloneDemo1 {
public static void main(String args[])
throws CloneNotSupportedException {
A obj1 = new A(37);
A obj2 = (A)obj1.clone();
}
}
这个代码引发一个编译错误,因为Object.clone是一个protected方法。那么再试一次,换一种方法:
class A {
private int x;
public A(int i) {
x = i;
}
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo2 {
public static void main(String args[])
throws CloneNotSupportedException {
A obj1 = new A(37);
A obj2 = (A)obj1.clone();
}
}
????
在这个方法中,你定义自己的clone方法,它扩展Object.clone方法,CloneDemo2可以编译,但是当你运行它时会抛出一个CloneNotSupportedException异常。
这里仍然缺少一些东西,你必须让那些包含clone方法的类实现Cloneable接口,就像这样:
class A implements Cloneable {
private int x;
public A(int i) {
x = i;
}
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
public int getx() {
return x;
}
}
public class CloneDemo3 {
public static void main(String args[])
throws CloneNotSupportedException {
A obj1 = new A(37);
A obj2 = (A)obj1.clone();
System.out.println(obj2.getx());
}
}
成功了!CloneDemo3可以编译并产生期望的结果:
37
你已经了解了必须显式的指定clone方法并且你的类必须实现Cloneable接口。Cloneable是“标记”接口的一个范例,接口自身不指定任何东西,但是,Object.clone检查类是否实现了它,如果没有就抛出一个CloneNotSupportedException异常。
Object.clone方法做简单的拷贝操作,将一个对象的所有成员变量拷贝到一个新的对象。在CloneDemo3中,A.clone调用Object.clone,然后Object.clone创建一个新的A对象并将已经存在的那个对象的成员变量的内容拷贝到那个新对象。
CloneDemo3中还有大量其他值得考虑的东西。其中之一就是你可以防止你写的类的用户拷贝那个类对象。为了做到这个,你可以不实现Cloneable接口,因此拷贝操作总会抛出异常。然而在大部分情况下为你的类规划和实现一个clone方法因而可以恰当的拷贝会更好。
另外一点就是你可以支持无条件的和有条件的拷贝。CloneDemo3是无条件的支持拷贝,clone方法不会传播CloneNotSupportedException异常。
一个更通用的方法是有条件的支持类拷贝。在这种情况下,对象自身可以被拷贝,但是对象的子类可能不能拷贝。对于有条件拷贝,clone方法必须申明它能够传播CloneNotSupportedException异常。有条件拷贝的一个范例是一个集合类的对象的元素只有在那些元素是可以拷贝的时候才能进行拷贝。
有条件拷贝的另外的一种方式是实现一个合适的clone方法但是不实现Cloneable接口。在这种情况下,如果愿意,子类可以支持拷贝操作。
拷贝操作可能是很棘手的,因为Object.clone做简单的对象成员拷贝,有时候这不是你期望的,例如:
import java.util.*;
class A implements Cloneable {
public HashMap map;
public A() {
map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
}
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo4 {
public static void main(String args[]) {
A obj1 = new A();
A obj2 = (A)obj1.clone();
obj1.map.remove("key1");
System.out.println(obj2.map.get("key1"));
}
}
你可能希望CloneDemo4显示如下的结果:
value1
但是实际上它显示:
null
发生了什么事?在CloneDemo4中,一个对象包含一个HashMap引用,当对象被拷贝时,HashMap 引用也被拷贝了,这意味着拷贝生成的那个对象包含那个HashMap对象的原始引用。因此当原始对象中的HashMap的内容发生变化,拷贝生成的对象中的那个HashMap的内容也同时更新。
要修正这个问题,你可以让clone方法更完善:
import java.util.*;
class A implements Cloneable {
public HashMap map;
public A() {
map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
}
public Object clone() {
try {
A aobj = (A)super.clone();
aobj.map = (HashMap)map.clone();
return aobj;
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo5 {
public static void main(String args[]) {
A obj1 = new A();
A obj2 = (A)obj1.clone();
obj1.map.remove("key1");
System.out.println(obj2.map.get("key1"));
}
}
????
Clone5Demo显示如下的期望的结果:
value1
Clone5Demo调用super.clone创建一个A对象并拷贝map成员,然后调用HashMap.clone完成HashMap类型的拷贝。这个操作包含创建一个新的hash表并且从老的那个里面拷贝成员到那个新的hash表。
如果两个对象共享一个引用,就像CloneDemo4中的情况一样,那么通常你会遇到问题,除非那个引用是只读的,要避开这个问题,你需要实现clone方法处理这个问题。这种情况的另一种说法是
Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。
不使用"new CloneDemo5"创建一个对象,那么调用super.clone就是极度重要的。你应该在类层次的每一级上调用super.clone。这是因为每一级都可能有它自己的共享对象问题。如果你使用"new"而不是super.clone,那么你的代码对于那些从你的类继承的子类是不正确的,那些代码调用你的clone方法但是收到一个不正确的返回类型。
关于拷贝,另一个需要知道的事情是可以拷贝任何数组,只需简单的调用clone方法:
public class CloneDemo6 {
public static void main(String args[]) {
int vec1[] = new int[]{1, 2, 3};
int vec2[] = (int[])vec1.clone();
System.out.println(vec2[0] + " " + vec2[1] +
" " + vec2[2]);
}
}
关于拷贝的最后一个重要的事情是:它是创建和初始化一个新对象的方式,但是它不同于调用一个构造方法。这个区别的一个例子是空的final成员,也就是那些声明为"final"但是没有初始化的成员,它们只能在构造方法中被赋值。下面是一个空的final成员的用法:
public class CloneDemo7 {
private int a;
private int b;
private final long c;
public CloneDemo7(int a, int b) {
this.a = a;
this.b = b;
this.c = System.currentTimeMillis();
}
public static void main(String args[]) {
CloneDemo7 obj = new CloneDemo7(37, 47);
}
}
在CloneDemo7的构造方法中,一个final成员"c"从系统时钟中获得一个时戳。如果你拷贝这样的类型的值你想得到什么?Object.clone拷贝所有的成员变量,但是你想那个时戳成员被设置为当前系统时钟的值。然而,如果一个成员是final类型的,你只能在构造方法中设置那个成员,不能在clone方法中。下面是这个问题的例子:
public class CloneDemo8 {
private int a;
private int b;
private final long c;
public CloneDemo8(int a, int b) {
this.a = a;
this.b = b;
this.c = System.currentTimeMillis();
}
public CloneDemo8(CloneDemo8 obj) {
this.a = obj.a;
this.b = obj.b;
this.c = System.currentTimeMillis();
}
public Object clone() throws CloneNotSupportedException {
//this.c = System.currentTimeMillis();
return super.clone();
}
public static void main(String args[]) {
CloneDemo8 obj = new CloneDemo8(37, 47);
CloneDemo8 obj2 = new CloneDemo8(obj);
}
}
如果你想取消final成员的赋值语句那一行的注释程序就不能编译。对于这样的问题,我们不使用clone方法,范例程序使用拷贝构造方法。拷贝构造方法的参数是和它自身类型相同并实现合适的拷贝逻辑。(译者注:在实现拷贝构造方法时需要注意共享对象问题,由于范例中的其他两个成员都是原始类型所以没有问题,但是如果你自己的类的成员的类型是对象类型就不能使用直接赋值也要使用拷贝进行或者是其他合适的拷贝构造方法,但是如果你需要使用的类型没有拷贝方法或者合适的拷贝构造方法,那么你就不能写你自己的合适的拷贝构造方法或者拷贝方法,所辛的是java的核心类基本上不存在这个问题,但是你如果使用其他的人的类就不好说了,因此如果你写自己的类并想让很多人用,那么你一定要实现合适的拷贝方法)
也许你认为你可以不使用空final成员而是在声明那些final成员的时候马上使用系统时间来初始化解决这样的问题,就像下面这样:
class A implements Cloneable {
final long x = System.currentTimeMillis();
public Object clone() {
try {
return super.clone();
}
catch (CloneNotSupportedException e) {
throw new InternalError(e.toString());
}
}
}
public class CloneDemo9 {
public static void main(String args[]) {
A obj1 = new A();
// sleep 100 ms before doing clone,
// to ensure unique timestamp
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
System.err.println(e);
}
A obj2 = (A)obj1.clone();
System.out.println(obj1.x + " " + obj2.x);
}
}
这样同样不能工作,当你运行这个程序,你可以看到obj1.x和obj2.x有相同的值。这指出当一个对象是拷贝生成的时候,通常的对象初始化工作没有进行并且你不能在clone方法中设置final成员的值。因此如果简单的拷贝操作不能正确的初始化一个成员,你就不应该将它声明为final的。或者你需要使用拷贝构造方法作为拷贝的替代方法。(译者注:如果你将成员声明为private并且不提供修改它的值方法,那么效果和将它声明为final是相同的) |
|