1. 简介
在Java中拷贝对象有深拷贝和浅拷贝两种。本文将对这两种方法进行比较,并学习四种实现深拷贝的方法。
2. Maven依赖
后文实现深拷贝会依赖两个工具 Gson、Jackson 及 apache commons lang。为了进行单元测试我们这里使用assertj流式断言库。
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
</dependency>
3.Model
为了比较不同拷贝Java对象的方法,我们需要使用两个类作为例子进行演示。
class Address {
private String street;
private String city;
private String country;
// standard constructors, getters and setters
}
class User {
private String firstName;
private String lastName;
private Address address;
// standard constructors, getters and setters
}
4.浅拷贝
浅拷贝仅将一个对象中字段的值拷贝到另一个字段。
@Test
public void whenShallowCopying_thenObjectsShouldNotBeSame() {
Address address = new Address("Downing St 10", "London", "England");
User pm = new User("Prime", "Minister", address);
User shallowCopy = new User(
pm.getFirstName(), pm.getLastName(), pm.getAddress());
assertThat(shallowCopy)
.isNotSameAs(pm);
}
本例中,pm != shallowCopy。他们是不同的对象。但当我们改变原始对象中的地址属性时,也会对浅拷贝生成的对象中的address产生影响。
5.深拷贝
与浅拷贝不同,深拷贝会递归复制引用对象。最终一个对象属性修改不会影响另一个通过深拷贝生成的对象。接下来将演示几种深拷贝的实现。
5.1 拷贝构造器
第一种实现将基于拷贝构造函数。
public Address(Address that) {
this(that.getStreet(), that.getCity(), that.getCountry());
}
public User(User that) {
this(that.getFirstName(), that.getLastName(), new Address(that.getAddress()));
}
@Test
public void whenModifyingOriginalObject_thenCopyShouldNotChange() {
Address address = new Address("Downing St 10", "London", "England");
User pm = new User("Prime", "Minister", address);
User deepCopy = new User(pm);
address.setCountry("Great Britain");
assertNotEquals(
pm.getAddress().getCountry(),
deepCopy.getAddress().getCountry());
}
这种实现通过手工的方式将类的各个成员进行赋值。String是不可变类,因此不需要创建对象的构造器来赋值。
创建完毕后,修改一个对象不会影响另一个对象中的值。
5.2 Cloneable接口
第二种实现基于Object对象的clone方法。该方法由protected修饰,我们需要覆盖并调整为public。接着我们在Address类中增加clone()方法。在clone方法内创建深拷贝对象。
@Override
public Object clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
return new Address(this.street, this.getCity(), this.getCountry());
}
}
接着我们来实现User类的clone方法。
@Override
public Object clone() {
User user = null;
try {
user = (User) super.clone();
} catch (CloneNotSupportedException e) {
user = new User(
this.getFirstName(), this.getLastName(), this.getAddress());
}
user.address = (Address) this.address.clone();
return user;
}
super.clone()返回一个浅拷贝对象,但是我们通过调用address的clone方法返回一个深拷贝对象最终对User实现深拷贝。
@Test
public void whenModifyingOriginalObject_thenCloneCopyShouldNotChange() {
Address address = new Address("Downing St 10", "London", "England");
User pm = new User("Prime", "Minister", address);
User deepCopy = (User) pm.clone();
address.setCountry("Great Britain");
assertThat(deepCopy.getAddress().getCountry())
.isNotEqualTo(pm.getAddress().getCountry());
}
6.外部库
上述方法通常情况下在处理第三方依赖的类时无法使用,因为我们获取第三方代码及修改第三方代码代价较高。因此可以考虑使用一些第外部工具库来解决这些问题。
6.1 Apache Commons Lang
Apache Commons Lang有一个SerializationUtils#clone方法,该方法会执行深拷贝。但所拷贝的对象及其依赖的对象必须实现Serializable接口。如果尝试clone一个对象,其依赖实现serializable的对象,那么方法将会抛出异常。
@Test
public void whenModifyingOriginalObject_thenCommonsCloneShouldNotChange() {
Address address = new Address("Downing St 10", "London", "England");
User pm = new User("Prime", "Minister", address);
User deepCopy = (User) SerializationUtils.clone(pm);
address.setCountry("Great Britain");
assertThat(deepCopy.getAddress().getCountry())
.isNotEqualTo(pm.getAddress().getCountry());
}
6.2 通过Gson进行JSON序列化JSON Serialization With Gson
Gson可以用来将对象转换为JSON,反之亦然。与Apache Commons Lang不同,GSON在转换时不需要对象实现Serializable接口。因此我们可以将对象先序列化为JSON字符串,随后再将JSON字符串反序列化为对象。这样实现对象的深拷贝。
@Test
public void whenModifyingOriginalObject_thenGsonCloneShouldNotChange() {
Address address = new Address("Downing St 10", "London", "England");
User pm = new User("Prime", "Minister", address);
Gson gson = new Gson();
User deepCopy = gson.fromJson(gson.toJson(pm), User.class);
address.setCountry("Great Britain");
assertThat(deepCopy.getAddress().getCountry())
.isNotEqualTo(pm.getAddress().getCountry());
}
6.3 通过jackson进行JSON序列化
jackson也是一个库,支持JSON的序列化。但使用jackson时,需要给我们的类增加默认的构造器。其处理思路和GSON是一样的。
@Test
public void whenModifyingOriginalObject_thenJacksonCopyShouldNotChange()
throws IOException {
Address address = new Address("Downing St 10", "London", "England");
User pm = new User("Prime", "Minister", address);
ObjectMapper objectMapper = new ObjectMapper();
User deepCopy = objectMapper
.readValue(objectMapper.writeValueAsString(pm), User.class);
address.setCountry("Great Britain");
assertThat(deepCopy.getAddress().getCountry())
.isNotEqualTo(pm.getAddress().getCountry());
}
7.总结
本文介绍了深拷贝的方法及如何通过第三方库去进行拷贝。实际应用可根据情况选择使用。