前段时间碰到需要将一个Java对象进行深度拷贝的情况,但是JDK并未提供关于deep copy相关的API,唯一能用的就是一个不太稳定的clone(),所以问题就来了,如何实现稳定的deep copy,下面就实现deep copy的方法做个介绍。
实现deep copy,首先想到的是可以直接赋值么?如下:
Test test = new Test();
Test test2 = test;
System.out.println(test);
System.out.println(test2);
上面的代码里,直接将test复制给test2,但是将两个对象打印出来发现,地址其实是一样的,test只是刚刚在堆上分配的Test对象的引用,而这里的赋值直接是引用直接的赋值,等于test2也是指向刚刚new出来的对象,这里的copy就是一个shallow copy,及只是copy了一份引用,但是对象实体并未copy,既然赋值不行,那就试试第二个方法,Object类的clone方法。
Java中所有对象都继承自Object类,所以就默认自带clone方法的实现,clone方法的实现是比较简单粗暴的。首先,如果一个对象想要调用clone方法,必须实现Cloneable接口,否则会抛出CloneNotSupportedException。其实这个Cloneable是个空接口,只是个flag用来标记这个类是可以clone的,所以说将一个类声明为Cloneable与这个类具备clone能力其实并不是直接相关的。其实Cloneable是想表明具有复制这种功能,所以按理说clone应该作为Cloneable的一个方法而存在,但是实际上clone方法是Object类的一个protected方法,所以你无法直接通过多态的方式调用clone方法,比如:
public class Test implements Cloneable {
public static void main(String[] args) {
try {
List list = new ArrayList();
Cloneable t1 = new InnerTest("test");
list.add(t1);
list.add(t1.clone()); // 事实上,我无法这么做
} catch (Exception e) {
e.printStackTrace();
}
}
public static class InnerTest implements Cloneable {
public String a;
public InnerTest(String test) {
a = test;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
这其实是设计上的一个缺陷,不过导致clone方法声名狼藉的并不单单因为这个。
当调用clone方法时,首先会直接分配内存,然后将原对象内所有的字段都一一复制,如果字段是基本类型数据比如int之类的,则这样直接的赋值式的复制毫无问题,但是如果字段是引用的话问题就来了,引用也会原封不动的复制一份,就如同第一个例子一样。所以,很多情景下,clone只能算一个半deep半shallow的复制方法。想要解决这个问题,唯一的方法就是在需要被复制的对象的clone方法内调用会被shallow copy的对象的clone方法,但是前提是该对象也继承了Cloneable接口并Override了clone方法。比如:
public class Test implements Cloneable {
public static void main(String[] args) {
try {
InnerTest t1 = new InnerTest(new InnerTest2());
InnerTest t2 = (InnerTest) t1.clone();
System.out.println(t1); // Test$InnerTest@232204a1
System.out.println(t2); // Test$InnerTest@4aa298b7
} catch (Exception e) {
e.printStackTrace();
}
}
public static class InnerTest implements Cloneable {
public InnerTest2 test;
public InnerTest(InnerTest2 test) {
this.test = test;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static class InnerTest2 implements Cloneable {
public InnerTest2() {
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
此外,clone方法不通过构造函数来创建新对象,所以构造函数内的逻辑也会被直接跳过,这也会带来问题,等于clone引进了一个我们无法控制的对象构造方法。比如想在构造函数内实现一个计数功能,每次new就加1,但是如果clone的话,则这个计数就无法生效。比如:
public class Test implements Cloneable {
public static void main(String[] args) {
try {
List list = new ArrayList();
InnerTest t1 = new InnerTest("test");
InnerTest t2 = new InnerTest("test1");
list.add(t1);
list.add(t2);
list.add((Cloneable) t1.clone());
for (Cloneable c : list) {
System.out.println(((InnerTest) c).index ); // 依次打印 0 1 0
}
System.out.println(InnerTest.count); // count为2
} catch (Exception e) {
e.printStackTrace();
}
}
public static class InnerTest implements Cloneable {
public int index;
public static int count = 0;
public InnerTest(String test) {
index = count;
count++;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
此外也可以自己实现一套Copyable接口,然后想要复制的类都继承该接口并复现copy函数即可。但是copy函数内的逻辑其实与复制构造类似。比如:
Copyable接口:
public interface Copyable {
T copy ();
}
具体实现与测试:
public class Test{
public static void main(String[] args) {
try {
InnerTest t1 = new InnerTest(new InnerTest2());
InnerTest t2 = t1.copy();
System.out.println(t1.test.getA()); // print 0
t1.test.setA(5);
System.out.println(t2.test.getA()); // print 0
} catch (Exception e) {
e.printStackTrace();
}
}
// 测试类
public static class InnerTest implements Copyable {
// set to public for convenience
public InnerTest2 test;
public InnerTest(InnerTest2 tmp) {
this.test = tmp;
}
@Override
public InnerTest copy() {
InnerTest2 tmp = test == null ? null : test.copy();
return new InnerTest(tmp);
}
}
// 测试类,增加getter和setter方法来验证
public static class InnerTest2 implements Copyable{
private int a;
public InnerTest2() {
a = 0;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
@Override
public InnerTest2 copy() {
InnerTest2 tmp = new InnerTest2();
tmp.setA(this.a);
return tmp;
}
}
}
其实大部分情况下复制构造是个不错的选择,但是实现上来说确实比较繁琐,且容易出错,因为需要递归式的将所有的对象和它引用的对象都进行复制,所以就有了另外一种实现deep copy的思路:Java Object Serialization (JOS)。序列化会将一个对象的各个方面都考虑到,包括父类,各个字段,以及各种引用。所以如果将一个对象先序列化写入字节流,然后再读出,重新构造成一个对象,就能实现这个对象的deep copy。当然,这里其实也没考虑构造函数逻辑,但是这种方法却不需要考虑会有shallow copy的可能,而且省去了繁琐的复制构造或者copy方法的覆写,我们可以直接通过一个实现一个deepCopy函数来实现对象复制。下面就对这种方法做一个介绍。
public class Test2 {
public static Object deepCopy(Object from) {
Object obj = null;
try {
// 将对象写成 Byte Array
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(from);
out.flush();
out.close();
// 从流中读出 byte array,调用readObject函数反序列化出对象
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
obj = in.readObject();
} catch(IOException e) {
e.printStackTrace();
} catch(ClassNotFoundException e2) {
e2.printStackTrace();
}
return obj;
}
}
通过上面的例子,我们之间调用deepCopy函数就可以将一个对象进行deep copy并且返回一个新的对象。这里的writeObject和readObject分别将对象序列化和反序列化。