引言
在java中,当我们对一个对象进行复制时,有两种可以考虑的方式-浅复制和深复制
当我们仅需要复制字段值时,依赖于原始对象进行浅复制是一种方式。在深复制中,确保树中所有的对象都被深度复制,因此该副本不依赖任何以前可能更改的现有对象。
接下来,我们将比较这两种方式,并实现深复制的四种方法。
Maven依赖
我们将使用这三个依赖包-Gson、Jackson、Apache Commons Lang来进行不同深度复制的测试。
添加这三个依赖的Maven坐标到pom.xml文件中。
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.2</version>
</dependency>
这些依赖包的最新版都可以在Maven Central中找到。
对象
为了方便进行测试,我们需要创建一下两个对象:
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
}
潜复制
潜复制是我们将一个对象的字段值复制到另外一个对象中:
@Test
public void shallowCopy() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
User shallowCopy = new User(pm.getFirstName(), pm.getLastName(), pm.getAddress());
assertNotSame(pm, shallowCopy);
}
执行代码,我们可以看到pm != shallowCopy,意味着pm和shallowCopy是不同的对象,但是问题在于,如果我们修改了pm的地址,这也会影响到shallowCopy的地址信息。
如果Address不会发生改变,我们将不会被它影响,但是事实并非如此:
@Test
public void modifyingOriginalAddress() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
User shallowCopy = new User(pm.getFirstName(), pm.getLastName(), pm.getAddress());
address.setStreet("光谷大道");
assertEquals(shallowCopy.getAddress().getStreet(), pm.getAddress().getStreet());
}
深复制
深复制是解决此问题的替代方法,它的优点是:对象树中至少每个可变的对象都是递归复制的。
由于深复制不依赖于先前创建的任何可变对象,因此不会像我们在浅复制中看到的那样,意外进行了修改。
下面我们将展示几种深复制的实现,并展示这种优势。
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是一个不可变的类。
因此,不能被意外修改。下面让我们看看这是否可行:
/**
* Copy Constructor
*/
@Test
public void modifyingOriginalAddress_copyConstructor() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
User deepCopy = new User(pm);
address.setCountry("光谷大道");
assertNotEquals(
pm.getAddress().getCountry(),
deepCopy.getAddress().getCountry());
}
Cloneable Interface
第二种实现基于从Object继承的clone方法,我们需要覆盖它。
我们还要在类实现Cloneable接口,表示当前类是可以克隆的。
在Address类中实现clone()方法:
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
return new Address(this.street, this.getCity(), this.getCountry());
}
}
现在让我们为Use类实现clone()方法:
@Override
public User clone() {
User user;
try {
user = (User) super.clone();
} catch (CloneNotSupportedException e) {
user = new User(
this.getFirstName(), this.getLastName(), this.getAddress());
}
user.address = this.address.clone();
return user;
}
注意,super.clone()调用返回了对象的浅复制副本,但是我们手动设置了可变字段的深复制副本,因此结构是正确的:
@Test
public void modifyingOriginalAddress_clone() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
User deepCopy = pm.clone();
address.setCountry("光谷大道");
assertNotEquals(
pm.getAddress().getCountry(),
deepCopy.getAddress().getCountry());
}
第三方库
上的示例看起来很简单,当我们不能添加其他的构造函数或覆盖clone方式时,上面的解决方案就行不通了。
当我们需要复制的对象不能修改,或者对象太复杂以至于如果编写额外的构造函数或者在对象的所有类上实现clone方法时,我们无法按时完成项目,我们就需要使用第三方库。
在这种情况下,我们可以使用其他第三方库。为了深复制,我们可以序列化一个对象,然后将其反序列化为一个新的对象。
让我们看看下面的几个例子。
Apache Commons Lang
Apache Commons Lang中SerializationUtils类有个clone方法,当对象的类都实现Serializable接口时,它将执行深复制。
如果该方法遇到无法序列化的类,则它将失败并抛出未经检查的SerializationException。
因此,我们需要将Serializable接口添加到我们的类中:
@Test
public void modifyingOriginalAddress_apacheCommonsLang() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
User deepCopy = SerializationUtils.clone(pm);
address.setCountry("光谷大道");
assertNotEquals(
pm.getAddress().getCountry(),
deepCopy.getAddress().getCountry());
}
使用Gson进行JSON序列化
序列化的另一种方法是使用JSON序列化。 Gson是一个用于将对象转换为JSON(反之亦然)的库。
与Apache Commons Lang不同,GSON不需要Serializable接口即可进行转换。 让我们快速看一个例子:
@Test
public void modifyingOriginalAddress_gson() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
Gson gson = new Gson();
User deepCopy = gson.fromJson(gson.toJson(pm), User.class);
address.setCountry("光谷大道");
assertNotEquals(
pm.getAddress().getCountry(),
deepCopy.getAddress().getCountry());
}
用Jackson进行JSON序列化
Jackson是另一个支持JSON序列化的库。这种实现与使用Gson的实现非常相似,但是我们需要将默认的无参构造函数添加到我们的Address、User类中。
让我们来看一个例子:
@Test
public void modifyingOriginalAddress_jackson() {
Address address = new Address("民族大道", "武汉市", "中国");
User pm = new User("zhao", "xin", address);
ObjectMapper objectMapper = new ObjectMapper();
User deepCopy = null;
try {
deepCopy = objectMapper.readValue(objectMapper.writeValueAsString(pm), User.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
address.setCountry("光谷大道");
assertNotEquals(
pm.getAddress().getCountry(),
deepCopy.getAddress().getCountry());
}
结论
进行深拷贝时应使用哪种实现?最终决定通常取决于我们将复制的类以及我们是否拥有这些类。