在Java语言里,当我们需要拷贝一个对象时,有两种类型的拷贝:浅拷贝与深拷贝。浅拷贝只是拷贝了源对象的地址,所以源对象的值发生变化时,拷贝对象的值也会发生变化。而深拷贝则是拷贝了源对象的所有值,所以即使源对象的值发生变化时,拷贝对象的值也不会改变。如下图描述:
了解了浅拷贝和深拷贝的区别之后,本篇博客将教大家几种深拷贝的方法。
拷贝对象
首先,我们定义一下需要拷贝的简单对象。
如上述代码,我们定义了一个User用户类,包含name姓名,和address地址,其中address并不是字符串,而是另一个Address类,包含country国家和city城市。构造方法和成员变量的get()、set()方法此处我们省略不写。接下来我们将详细描述如何深拷贝User对象。
方法一 构造函数
我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。
测试用例
方法二 重载clone()方法
Object父类有个clone()的拷贝方法,不过它是protected类型的,我们需要重写它并修改为public类型。除此之外,子类还需要实现Cloneable接口来告诉JVM这个类是可以拷贝的。
重写代码
让我们修改一下User类,Address类,实现Cloneable接口,使其支持深拷贝。
需要注意的是,super.clone()其实是浅拷贝,所以在重写User类的clone()方法时,address对象需要调用address.clone()重新赋值。
测试用例
方法三 Apache Commons Lang序列化
Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。
重写代码
让我们修改一下User类,Address类,实现Serializable接口,使其支持序列化。
测试用例
方法四 Gson序列化
Gson可以将对象序列化成JSON,也可以将JSON反序列化成对象,所以我们可以用它进行深拷贝。
测试用例
方法五 Jackson序列化
Jackson与Gson相似,可以将对象序列化成JSON,明显不同的地方是拷贝的类(包括其成员变量)需要有默认的无参构造函数。
重写代码
让我们修改一下User类,Address类,实现默认的无参构造函数,使其支持Jackson。
测试用例
总结
说了这么多深拷贝的实现方法,哪一种方法才是最好的呢?最简单的判断就是根据拷贝的类(包括其成员变量)是否提供了深拷贝的构造函数、是否实现了Cloneable接口、是否实现了Serializable接口、是否实现了默认的无参构造函数来进行选择。如果需要详细的考虑,则可以参考下面的表格:
转载请注明出处 /xinruyi 喜欢可以点击关注我
在有些业务场景下,我们需要两个完全相同却彼此无关的java对象。比如使用原型模式、多线程编程等。对此,java提供了深拷贝的概念。通过深度拷贝可以从源对象完美复制出一个相同却与源对象彼此独立的目标对象。这里的相同是指两个对象的状态和动作相同,彼此独立是指改变其中一个对象的状态不会影响到另外一个对象。实现深拷贝常用的实现方式有2种:Serializable,Cloneable。
Serializable方式就是通过java对象的序列化和反序列化的操作实现对象拷贝的一种比较常见的方式。本来java对象们都待在虚拟机堆中,通过序列化,将源对象的信息以另外一种形式存放在了堆外。这时源对象的信息就存在了2份,一份在堆内,一份在堆外。然后将堆外的这份信息通过反序列化的方式再放回到堆中,就创建了一个新的对象,也就是目标对象。
--Serializable代码
public staticObject cloneObjBySerialization(Serializable src)
{
Object dest= null;try{
ByteArrayOutputStream bos= null;
ObjectOutputStream oos= null;try{
bos= newByteArrayOutputStream();
oos= newObjectOutputStream(bos);
oos.writeObject(src);
oos.flush();
}finally{
oos.close();
}byte[] bytes =bos.toByteArray();
ObjectInputStream ois= null;try{
ois= new ObjectInputStream(newByteArrayInputStream(bytes));
dest=ois.readObject();
}finally{
ois.close();
}
}catch(Exception e)
{
e.printStackTrace();//克隆失败
}returndest;
}
源对象类型及其成员对象类型需要实现Serializable接口,一个都不能少。
importjava.io.Serializable;public class BattleShip implementsSerializable
{String name;
ClonePilot pilot;
BattleShip(String name, ClonePilot pilot)
{this.name =name;this.pilot =pilot;
}
}//ClonePilot类型实现了Cloneable接口,不过这对通过Serializable方式拷贝对象没有影响
public class ClonePilot implementsSerializable,Cloneable
{
String name;
String sex;
ClonePilot(String name, String sex)
{this.name =name;this.sex =sex;
}publicClonePilot clone()
{try{
ClonePilot dest= (ClonePilot)super.clone();returndest;
}catch(Exception e)
{
e.printStackTrace();
}return null;
}
}
最后,执行测试代码,查看结果。
public static voidmain(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 implementsCloneable
{
String name;
ClonePilot pilot;
Cruiser(String name, ClonePilot pilot)
{this.name =name;this.pilot =pilot;
}//Object.clone方法是protected修饰的,无法在外部调用。所以这里需要重载clone方法,改为public修饰,并且处理成员变量浅拷贝的问题。
publicCruiser clone()
{try{
Cruiser dest= (Cruiser)super.clone();
dest.pilot= this.pilot.clone();returndest;
}catch(Exception e)
{
e.printStackTrace();
}return null;
}
}
public class ClonePilot implementsSerializable,Cloneable
{
String name;
String sex;
ClonePilot(String name, String sex)
{this.name =name;this.sex =sex;
}//因为所有成员变量都是基本类型,所以只需要调用Object.clone()即可
publicClonePilot clone()
{try{
ClonePilot dest= (ClonePilot)super.clone();returndest;
}catch(Exception e)
{
e.printStackTrace();
}return null;
}
}
下面测试一下
public static voidmain(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框架的使用指南,敬请期待。(绝不咕咕)