java深拷贝!

前段时间碰到需要将一个Java对象进行深度拷贝的情况,但是JDK并未提供关于deep copy相关的API,唯一能用的就是一个不太稳定的clone(),所以问题就来了,如何实现稳定的deep copy,下面就实现deep copy的方法做个介绍。

1. 直接赋值

实现deep copy,首先想到的是可以直接赋值么?如下:

[java]  view plain  copy
  1. Test test = new Test();  
  2. Test test2 = test;  
  3.   
  4. System.out.println(test);  
  5. System.out.println(test2);  

上面的代码里,直接将test复制给test2,但是将两个对象打印出来发现,地址其实是一样的,test只是刚刚在堆上分配的Test对象的引用,而这里的赋值直接是引用直接的赋值,等于test2也是指向刚刚new出来的对象,这里的copy就是一个shallow copy,及只是copy了一份引用,但是对象实体并未copy,既然赋值不行,那就试试第二个方法,Object类的clone方法。

2. clone方法

1. clone方法介绍

Java中所有对象都继承自Object类,所以就默认自带clone方法的实现,clone方法的实现是比较简单粗暴的。首先,如果一个对象想要调用clone方法,必须实现Cloneable接口,否则会抛出CloneNotSupportedException。其实这个Cloneable是个空接口,只是个flag用来标记这个类是可以clone的,所以说将一个类声明为Cloneable与这个类具备clone能力其实并不是直接相关的。其实Cloneable是想表明具有复制这种功能,所以按理说clone应该作为Cloneable的一个方法而存在,但是实际上clone方法是Object类的一个protected方法,所以你无法直接通过多态的方式调用clone方法,比如:

[java]  view plain  copy
  1. public class Test implements Cloneable {  
  2.   
  3.     public static void main(String[] args) {  
  4.         try {  
  5.             List<Cloneable> list = new ArrayList<Cloneable>();  
  6.             Cloneable t1 = new InnerTest("test");  
  7.             list.add(t1);  
  8.             list.add(t1.clone()); // 事实上,我无法这么做  
  9.         } catch (Exception e) {  
  10.             e.printStackTrace();  
  11.         }  
  12.     }  
  13.   
  14.     public static class InnerTest implements Cloneable {  
  15.         public String a;  
  16.   
  17.         public InnerTest(String test) {  
  18.             a = test;  
  19.         }  
  20.         public Object clone() throws CloneNotSupportedException {  
  21.             return super.clone();  
  22.         }  
  23.     }  
  24. }  
这其实是设计上的一个缺陷,不过导致clone方法声名狼藉的并不单单因为这个。

2. clone是深复制还是浅复制

当调用clone方法时,首先会直接分配内存,然后将原对象内所有的字段都一一复制,如果字段是基本类型数据比如int之类的,则这样直接的赋值式的复制毫无问题,但是如果字段是引用的话问题就来了,引用也会原封不动的复制一份,就如同第一个例子一样。所以,很多情景下,clone只能算一个半deep半shallow的复制方法。想要解决这个问题,唯一的方法就是在需要被复制的对象的clone方法内调用会被shallow copy的对象的clone方法,但是前提是该对象也继承了Cloneable接口并Override了clone方法。比如:

[java]  view plain  copy
  1. public class Test implements Cloneable {  
  2.   
  3.     public static void main(String[] args) {  
  4.         try {  
  5.             InnerTest t1 = new InnerTest(new InnerTest2());  
  6.             InnerTest t2 = (InnerTest) t1.clone();  
  7.             System.out.println(t1); // Test$InnerTest@232204a1  
  8.             System.out.println(t2); // Test$InnerTest@4aa298b7  
  9.         } catch (Exception e) {  
  10.             e.printStackTrace();  
  11.         }  
  12.     }  
  13.   
  14.     public static class InnerTest implements Cloneable {  
  15.         public InnerTest2 test;  
  16.   
  17.         public InnerTest(InnerTest2 test) {  
  18.             this.test = test;  
  19.         }  
  20.   
  21.         @Override  
  22.         public Object clone() throws CloneNotSupportedException {  
  23.             return super.clone();  
  24.         }  
  25.     }  
  26.   
  27.     public static class InnerTest2 implements Cloneable {  
  28.         public InnerTest2() {  
  29.         }  
  30.   
  31.         @Override  
  32.         public Object clone() throws CloneNotSupportedException {  
  33.             return super.clone();  
  34.         }  
  35.     }  
  36. }  

3. clone跳过构造函数

此外,clone方法不通过构造函数来创建新对象,所以构造函数内的逻辑也会被直接跳过,这也会带来问题,等于clone引进了一个我们无法控制的对象构造方法。比如想在构造函数内实现一个计数功能,每次new就加1,但是如果clone的话,则这个计数就无法生效。比如:

[java]  view plain  copy
  1. public class Test implements Cloneable {  
  2.   
  3.     public static void main(String[] args) {  
  4.         try {  
  5.             List<Cloneable> list = new ArrayList<Cloneable>();  
  6.             InnerTest t1 = new InnerTest("test");  
  7.             InnerTest t2 = new InnerTest("test1");  
  8.             list.add(t1);  
  9.             list.add(t2);  
  10.             list.add((Cloneable) t1.clone());  
  11.             for (Cloneable c : list) {  
  12.                 System.out.println(((InnerTest) c).index ); // 依次打印 0 1 0  
  13.             }  
  14.             System.out.println(InnerTest.count); // count为2  
  15.         } catch (Exception e) {  
  16.             e.printStackTrace();  
  17.         }  
  18.     }  
  19.   
  20.     public static class InnerTest implements Cloneable {  
  21.         public int index;  
  22.         public static int count = 0;  
  23.   
  24.         public InnerTest(String test) {  
  25.             index = count;  
  26.             count++;  
  27.         }  
  28.         public Object clone() throws CloneNotSupportedException {  
  29.             return super.clone();  
  30.         }  
  31.     }  
  32. }  

4. 最佳实践——复制构造函数或者自定义Copyable接口

另外clone方法本身也是线程不安全的。所以总结下来就是clone是很不靠谱的,所以主流的建议还是添加复制构造函数,这样虽然会比较麻烦一点,但是可控性强且可以实现deep copy。

此外也可以自己实现一套Copyable接口,然后想要复制的类都继承该接口并复现copy函数即可。但是copy函数内的逻辑其实与复制构造类似。比如:

Copyable接口:

[java]  view plain  copy
  1. public interface Copyable<T> {  
  2.     T copy ();  
  3. }  

具体实现与测试

[java]  view plain  copy
  1. public class Test{  
  2.     public static void main(String[] args) {  
  3.         try {  
  4.             InnerTest t1 = new InnerTest(new InnerTest2());  
  5.             InnerTest t2 = t1.copy();  
  6.             System.out.println(t1.test.getA()); // print 0  
  7.             t1.test.setA(5);  
  8.             System.out.println(t2.test.getA()); // print 0  
  9.         } catch (Exception e) {  
  10.             e.printStackTrace();  
  11.         }  
  12.     }  
  13.   
  14.     // 测试类  
  15.     public static class InnerTest implements Copyable<InnerTest> {  
  16.         // set to public for convenience  
  17.         public InnerTest2 test;  
  18.   
  19.         public InnerTest(InnerTest2 tmp) {  
  20.             this.test = tmp;  
  21.         }  
  22.   
  23.         @Override  
  24.         public InnerTest copy() {  
  25.             InnerTest2 tmp = test == null ? null : test.copy();  
  26.             return new InnerTest(tmp);  
  27.         }  
  28.     }  
  29.   
  30.     // 测试类,增加getter和setter方法来验证  
  31.     public static class InnerTest2 implements Copyable<InnerTest2>{  
  32.         private int a;  
  33.         public InnerTest2() {  
  34.             a = 0;  
  35.         }  
  36.   
  37.         public int getA() {  
  38.             return a;  
  39.         }  
  40.   
  41.         public void setA(int a) {  
  42.             this.a = a;  
  43.         }  
  44.   
  45.         @Override  
  46.         public InnerTest2 copy() {  
  47.             InnerTest2 tmp = new InnerTest2();  
  48.             tmp.setA(this.a);  
  49.             return tmp;  
  50.         }  
  51.     }  
  52. }  

3. 序列化实现深复制

1. 为什么使用序列化

其实大部分情况下复制构造是个不错的选择,但是实现上来说确实比较繁琐,且容易出错,因为需要递归式的将所有的对象和它引用的对象都进行复制,所以就有了另外一种实现deep copy的思路:Java Object Serialization (JOS)。序列化会将一个对象的各个方面都考虑到,包括父类,各个字段,以及各种引用。所以如果将一个对象先序列化写入字节流,然后再读出,重新构造成一个对象,就能实现这个对象的deep copy。当然,这里其实也没考虑构造函数逻辑,但是这种方法却不需要考虑会有shallow copy的可能,而且省去了繁琐的复制构造或者copy方法的覆写,我们可以直接通过一个实现一个deepCopy函数来实现对象复制。下面就对这种方法做一个介绍。

2. 深复制的实现

如何实现deepCopy函数,下面提供一个简单的例子:
[java]  view plain  copy
  1. public class Test2 {  
  2.     public static Object deepCopy(Object from) {  
  3.         Object obj = null;  
  4.         try {  
  5.             // 将对象写成 Byte Array  
  6.             ByteArrayOutputStream bos = new ByteArrayOutputStream();  
  7.             ObjectOutputStream out = new ObjectOutputStream(bos);  
  8.             out.writeObject(from);  
  9.             out.flush();  
  10.             out.close();  
  11.   
  12.             // 从流中读出 byte array,调用readObject函数反序列化出对象  
  13.             ObjectInputStream in = new ObjectInputStream(  
  14.                     new ByteArrayInputStream(bos.toByteArray()));  
  15.             obj = in.readObject();  
  16.         } catch(IOException e) {  
  17.             e.printStackTrace();  
  18.         } catch(ClassNotFoundException e2) {  
  19.             e2.printStackTrace();  
  20.         }  
  21.         return obj;  
  22.     }  
  23. }  
通过上面的例子,我们之间调用deepCopy函数就可以将一个对象进行deep copy并且返回一个新的对象。这里的writeObject和readObject分别将对象序列化和反序列化。

3.序列化存在的问题

这种方法看上去比较简单,但是其实仍然存在很多问题:
首先,想要实现序列化必须实现序列化接口,也就表示所有需要深复制的类都应该实现Serializable接口,不过这倒是比较容易解决。
第二,序列化操作比较慢,其实序列化和反序列化两个操作是比较耗时的,这虽然可以通过自己来实现一套writeObject和readObject来解决,但是这里始终都是瓶颈。
第三,序列化操作中ByteArrayInputStream和ByteArrayOutputStream是线程安全的,一般情况下这没什么问题,但是当本身业务中不涉及到多线程情况的话这就会拖慢deep copy的速度。
其中第二点实现比较麻烦且速度提升不明显,但是在不涉及多线程的情况下,第三条却可以得到改变,我们可以自己实现非线程安全的InputStream和OutputStream的子类去替换ByteArrayInputStream和ByteArrayOutputStream,从而提升速度。

4. 使用相关第三方库

前面说到的几种方案都是各有优缺点,要么就是实现比较繁琐,要么就是功能不够稳定,一般这个时候可以看下是否有相关功能的成熟的类库,事实是关于deep copy的第三方库很多,比如Dozer(https://github.com/DozerMapper/dozer),Kryo(https://github.com/EsotericSoftware/kryo),cloning(https://github.com/kostaskougios/cloning)等,使用成熟类库可以很快且高效的实现deep copy,具体的发放此处不赘述,直接看github上文档即可。

总结一下,实现deep copy,主要的方法有:
  1. 实现Cloneable接口并覆写clone方法
  2. 使用复制构造函数
  3. 自定义一个Copyable接口,然后为需要clone的类增加copy方法的具体实现
  4. 通过序列化方式将一个对象先序列化再反序列化得到一个deep copy的新对象
  5. 使用成熟第三方库,具体方法看文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值