一、简介
当我们想在 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.13.0</version> </dependency>
你也可以指定最新的版本,最新版本的 Gson,Jackson 和 Apache Commons Lang 可以在 Maven 仓库上找到。https://mvnrepository.com/
三、弄两个类
为了比较复制 Java 对象的不同方法,我们需要两个类来处理:
public class Address { private String street; private String city; private String country; // getters and setters 方法 // toString 方法 }
public class User { private String uname; private String job; private Address address; // getters and setters 方法 // toString 方法 }
四、浅拷贝
浅拷贝是指我们只将字段值从一个对象复制到另一个对象:
// 当浅拷贝时,对象不是同一个 @Test public void test() { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); User shallowCopy = new User( pm.getUname(), pm.getJob(), pm.getAddress()); assertThat(shallowCopy) .isNotSameAs(pm); }
在这种情况下,pm != shallowCopy,这意味着它们是不同的对象;但问题是,当我们更改任何原始地址的属性时,这也会影响 shallowCopy 的地址。
如果地址是不可变的,我们不会打扰它,但它不是:
// 当我们更改任何原始地址的属性时,浅拷贝会被影响 @Test public test2() { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); User shallowCopy = new User( pm.getUname(), pm.getJob(), pm.getAddress()); address.setCountry("US"); assertThat(shallowCopy.getAddress().getCountry()) .isEqualTo(pm.getAddress().getCountry()); }
五、深拷贝
深拷贝是解决此问题的替代方法。它的优点是对象图中的每个可变对象都是递归复制的。
由于副本不依赖于之前创建的任何可变对象,因此它不会像我们在浅层副本中看到的那样被意外修改。
在以下部分中,我们将讨论几种深层复制实现并演示此优势。
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 test3() { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); User deepCopy = new User(pm); address.setCountry("US"); assertNotEquals( pm.getAddress().getCountry(), deepCopy.getAddress().getCountry()); }
5.2 克隆接口
第二个实现基于从 Object 继承的克隆方法。它受到保护,但我们需要将其覆盖为公共。
我们还将向类添加一个标记接口 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.getUname(), this.getJob(), this.getAddress()); } user.address = (Address) this.address.clone(); return user; }
请注意,super.clone() 调用返回对象的浅拷贝,但我们手动设置了可变字段的深层副本,因此结果是正确的:
@Test public void test4() { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); User deepCopy = (User) pm.clone(); address.setCountry("US"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }
六、外部类
上面的例子看起来很简单,但有时当我们无法添加额外的构造函数或重写克隆方法时,它们不能作为解决方案。
当我们不拥有代码时,或者当对象图非常复杂以至于如果我们专注于编写其他构造函数或在对象图中的所有类上实现克隆方法时,我们可能会按时完成项目。
那么我们能做什么呢?在这种情况下,我们可以使用外部库。为了实现深层复制,我们可以序列化一个对象,然后将其反序列化为新对象。
让我们看几个例子。
6.1 Apache Commons Lang
Apache Commons Lang 具有 SerializationUtils#clone,当对象图中的所有类都实现 Serializable 接口时,它会执行深度复制。
如果该方法遇到不可序列化的类,它将失败并引发未经检查的序列化异常。
因此,我们需要将可序列化接口添加到我们的类中:
@Test public void test5() { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); User deepCopy = (User) SerializationUtils.clone(pm); address.setCountry("US"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }
6.2. 使用 gson 进行 JSON 序列化
序列化的另一种方法是使用 JSON 序列化。Gson 是一个用于将对象转换为 JSON 的库,反之亦然。
与 Apache Commons Lang 不同,GSON 不需要 Serializable 接口来进行转换。
让我们快速看一个例子:
@Test public void test6() { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); Gson gson = new Gson(); User deepCopy = gson.fromJson(gson.toJson(pm), User.class); address.setCountry("US"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }
6.3. 使用 Jackson 实现 JSON 序列化
Jackson 是另一个支持 JSON 序列化的库。这个实现与使用 Gson 的实现非常相似,但我们需要将默认构造函数添加到我们的类中。
让我们看一个例子:
@Test public void test7() throws IOException { Address address = new Address("天府路", "广州", "中国"); User pm = new User("cuihua", "manager", address); ObjectMapper objectMapper = new ObjectMapper(); User deepCopy = objectMapper .readValue(objectMapper.writeValueAsString(pm), User.class); address.setCountry("US"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }
七、总结一下
制作深度拷贝时应该使用哪种实现?最终的决定通常取决于我们要复制的类,以及我们是否拥有对象图中的类。