1. 引言
在Java中复制对象时,我们需要考虑两种方式:浅拷贝和深拷贝。
- 浅拷贝:仅复制字段值,导致拷贝对象可能依赖原始对象
- 深拷贝:递归复制对象树中的所有对象,确保拷贝完全独立
本文将对比这两种方式,并介绍四种实现深拷贝的方法。
2. Maven依赖配置
我们将使用三个Maven依赖测试不同深拷贝实现:Gson、Jackson和Apache Commons Lang。
在pom.xml
中添加以下依赖:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
最新版本可在Maven Central获取:
3. 模型类
为测试不同拷贝方法,我们定义两个类:
class Address {
private String street;
private String city;
private String country;
// 标准构造器、getter和setter
}
class User {
private String firstName;
private String lastName;
private Address address;
// 标准构造器、getter和setter
}
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
会影响shallowCopy
:
@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());
}
⚠️ 踩坑点:如果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()));
}
✅ 注意: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());
}
5.2. Cloneable接口
重写clone()
方法并实现Cloneable
接口:
@Override
public Object clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
return new Address(this.street, this.getCity(), this.getCountry());
}
}
@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());
}
6. 外部库方案
当无法修改源码(如第三方类)或对象图复杂时,可使用外部库实现深拷贝。
6.1. Apache Commons Lang
使用SerializationUtils#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());
}
❌ 限制:遇到非Serializable
类会抛出SerializationException
。
6.2. Gson JSON序列化
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());
}
✅ 优势:不依赖特定接口,适用性更广。
6.3. Jackson JSON序列化
Jackson要求类有无参构造器:
@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. 总结
选择深拷贝实现方式时需考虑:
是否拥有类控制权
- ✅ 可修改类:拷贝构造器或
Cloneable
- ❌ 不可修改类:外部库方案
- ✅ 可修改类:拷贝构造器或
对象图复杂度
- 简单对象:手动实现
- 复杂对象:JSON序列化更高效
性能要求
- 高频场景:拷贝构造器性能最佳
- 低频场景:JSON序列化开发效率更高
完整代码示例见GitHub仓库。