Java对象的深拷贝
综述
当我们想要在 Java 中复制一个对象时,我们需要考虑两种可能性,浅拷贝和深拷贝。
对于浅拷贝方法,我们只拷贝字段值,因此拷贝可能依赖于原始对象。在深度复制方法中,我们确保树中的所有对象都被深度复制,因此副本不依赖于任何可能会更改的先前存在的对象。
Maven设置
我们将使用三个Maven依赖项Gson、Jackson和Apache Commons Lang来测试深拷贝的不同方式。
<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>
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
}
浅拷贝
浅拷贝是一种只将字段的值从一个对象复制到另一个对象。A shallow copy is one in which we only copy values of fields from one object to another:
@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的属性时,这也会影响到shallowCopy的Address对象。
@Test
public void whenModifyingOriginalObject_ThenCopyShouldChange() {
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());
address.setCountry("Great Britain");
assertThat(shallowCopy.getAddress().getCountry())
.isEqualTo(pm.getAddress().getCountry());
}
深拷贝
深拷贝是解决此问题的替代方案。它的优点是对象图中的每个可变对象都是递归复制的(each mutable object in the object graph is recursively copied.)。
因为复制不依赖于之前创建的任何可变对象,所以它不会像我们在浅复制中看到的那样被意外修改。
复制构造函数(Copy Constructor)
public Address(Address that) {
this(that.getStreet(), that.getCity(), that.getCountry());
}
public User(User that) {
this(that.getFirstName(), that.getLastName(), new Address(that.getAddress()));
}
在上面的深拷贝实现中,我们没有在复制构造函数中创建新的String,因为String是一个不可变类,因此,它们不能被意外修改。
@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());
}
Cloneable接口
该实现基于从Object继承的clone方法。它是受保护(protected)的,但我们需要对它进行重写为public的。
我们还需要向类添加一个标记接口 Cloneable,以表明这些类实际上是cloneable的。
将clone()方法添加到Address类中:
@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()调用返回对象的浅层副本,但是我们手动设置可变字段的深层副本,因此结果是正确的。
@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());
}
External Libraries
上面的例子看起来很简单,但有时当我们不能添加额外的构造函数或重写克隆方法时,它们就不能作为解决方案。
比如当我们没有源代码时,或者当object graph非常复杂,如果我们专注于编写额外的构造函数或在对象图中的所有类上实现clone()方法。
为了实现深度复制,我们可以序列化一个对象,然后将其反序列化为一个新对象。
Apache Commons Lang
Apache Commons Lang 有 SerializationUtils#clone,当对象图中的所有类都实现 Serializable 接口时,它会执行深度复制。
如果该方法遇到不可序列化的类,它将失败并抛出未经检查的 SerializationException,因此,我们需要将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());
}
Gson的JSON序列化
与Apache Commons Lang不同,GSON不需要Serializable接口来进行转换。
@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());
}
Jackson的JSON序列化
此实现与使用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());
}