在有些业务场景下,我们需要两个完全相同却彼此无关的java对象。比如使用原型模式、多线程编程等。对此,java提供了深拷贝的概念。通过深度拷贝可以从源对象完美复制出一个相同却与源对象彼此独立的目标对象。这里的相同是指两个对象的状态和动作相同,彼此独立是指改变其中一个对象的状态不会影响到另外一个对象。实现深拷贝常用的实现方式有2种:Serializable,Cloneable。
Serializable方式就是通过java对象的序列化和反序列化的操作实现对象拷贝的一种比较常见的方式。本来java对象们都待在虚拟机堆中,通过序列化,将源对象的信息以另外一种形式存放在了堆外。这时源对象的信息就存在了2份,一份在堆内,一份在堆外。然后将堆外的这份信息通过反序列化的方式再放回到堆中,就创建了一个新的对象,也就是目标对象。
--Serializable代码
public static Object cloneObjBySerialization(Serializable src) { Object dest = null; try { ByteArrayOutputStream bos = null; ObjectOutputStream oos = null; try { bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); oos.writeObject(src); oos.flush(); } finally { oos.close(); } byte[] bytes = bos.toByteArray(); ObjectInputStream ois = null; try { ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); dest = ois.readObject(); } finally { ois.close(); } } catch(Exception e) { e.printStackTrace();//克隆失败 } return dest; }
源对象类型及其成员对象类型需要实现Serializable接口,一个都不能少。
import java.io.Serializable; public class BattleShip implements Serializable { String name; ClonePilot pilot; BattleShip(String name, ClonePilot pilot) { this.name = name; this.pilot = pilot; } } //ClonePilot类型实现了Cloneable接口,不过这对通过Serializable方式拷贝对象没有影响 public class ClonePilot implements Serializable,Cloneable { String name; String sex; ClonePilot(String name, String sex) { this.name = name; this.sex = sex; } public ClonePilot clone() { try { ClonePilot dest = (ClonePilot)super.clone(); return dest; } catch(Exception e) { e.printStackTrace(); } return null; } }
最后,执行测试代码,查看结果。
public static void main(String[] args)
{ BattleShip bs = new BattleShip("Dominix", new ClonePilot("Alex", "male")); System.out.println(bs); System.out.println(bs.name + " "+bs.pilot.name); BattleShip cloneBs = (BattleShip)CloneObjUtils.cloneObjBySerialization(bs); System.out.println(cloneBs); System.out.println(cloneBs.name + " "+cloneBs.pilot.name); }
console--output--
cloneObject.BattleShip@154617c
Dominix Alex
cloneObject.BattleShip@cbcfc0
Dominix Alex
cloneObject.ClonePilot@a987ac
cloneObject.ClonePilot@1184fc6
从控制台的输出可以看到,两个不同的BattleShip对象,各自引用着不同的Clonepilot对象。String作为不可变类,这里可以作为基本类型处理。该有的数据都有,两个BattleShip对象也没有引用同一个成员对象的情况。表示深拷贝成功了。
注意序列化会忽略transient修饰的变量。所以这种方式不会拷贝transient修饰的变量。
另外一种方式是Cloneable,核心是Object类的native方法clone()。通过调用clone方法,可以创建出一个当前对象的克隆体,但需要注意的是,这个方法不支持深拷贝。如果对象的成员变量是基础类型,那妥妥的没问题。但是对于自定义类型的变量或者集合(集合我还没测试过)、数组,就有问题了。你会发现源对象和目标对象的自定义类型成员变量是同一个对象,也就是浅拷贝,浅拷贝就是对对象引用(地址)的拷贝。这样的话源对象和目标对象就不是彼此独立,而是纠缠不休了。为了弥补clone方法的这个不足。需要我们自己去处理非基本类型成员变量的深拷贝。
--Cloneable代码
public class Cruiser implements Cloneable { String name; ClonePilot pilot; Cruiser(String name, ClonePilot pilot) { this.name = name; this.pilot = pilot; } //Object.clone方法是protected修饰的,无法在外部调用。所以这里需要重载clone方法,改为public修饰,并且处理成员变量浅拷贝的问题。 public Cruiser clone() { try { Cruiser dest = (Cruiser)super.clone(); dest.pilot = this.pilot.clone(); return dest; } catch(Exception e) { e.printStackTrace(); } return null; } }
public class ClonePilot implements Serializable,Cloneable { String name; String sex; ClonePilot(String name, String sex) { this.name = name; this.sex = sex; } //因为所有成员变量都是基本类型,所以只需要调用Object.clone()即可 public ClonePilot clone() { try { ClonePilot dest = (ClonePilot)super.clone(); return dest; } catch(Exception e) { e.printStackTrace(); } return null; } }
下面测试一下
public static void main(String[] args) { Cruiser cruiser = new Cruiser("VNI", new ClonePilot("Alex", "male")); System.out.println(cruiser); Cruiser cloneCruiser = cruiser.clone(); System.out.println(cloneCruiser); System.out.println(cruiser.pilot); System.out.println(cloneCruiser.pilot); System.out.println(cruiser.pilot.name); System.out.println(cloneCruiser.pilot.name); }
执行结果如下:
cloneObject.Cruiser@1eba861 cloneObject.Cruiser@1480cf9 cloneObject.ClonePilot@1496d9f cloneObject.ClonePilot@3279cf Alex Alex
同样,从控制台的输出可以看到,两个不同的Cruiser对象,各自引用着不同的Clonepilot对象。该有的数据都有,两个Cruiser对象也没有引用同一个成员对象的情况。表示深拷贝成功了。
工作中遇到的大多是Serializable方式,这种方式代码量小,不容易出错。使用Cloneable方式需要对源对象的数据结构有了足够的了解才可以,代码量大,涉及的文件也多。虽然他们都需要源对象类型及其引用的成员对象类型实现相应的接口,不过一般情况下问题也不大。但是我曾有幸遇到过一次需要深拷贝的场景,源对象的某个成员变量类型没有实现任何接口,而且不允许我对此做任何修改。就在我黔驴技穷一筹莫展之际,我看到了光(kryo)。kryo是一个java序列化的框架,特别之处在于他不需要源对象类型实现任何接口,完美的解决了我的问题。后续我会写一篇kryo框架的使用指南,敬请期待。(绝不咕咕)