如何對JAVA物件進行深拷貝

someecho發表於2018-12-10

引自:How to Make a Deep Copy of an Object in Java

1. 介紹

當我們想克隆一個物件時,有兩種解決方案--- 淺拷貝(shadow copy)或深拷貝(deep clone)。
淺拷貝只拷貝物件的成員變數,因此和原物件有依賴關係;而使用深拷貝時,必須確保所有的物件樹(object tree)都被複制,使得拷貝體不依賴與原來存在的物件。
在此文中,我們將比較者兩種拷貝方式,學習4種深拷貝的方法。
複製程式碼

2.Maven依賴

我們將使用Gson、JackJon和Apache Commons Lang的依賴,測試深拷貝不同實現方式的表現。
以下是maven依賴的pom.xml
複製程式碼
<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>
複製程式碼

3. Model

先定義一下Model

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
}
複製程式碼

淺拷貝 Shadow Copy

以下演示淺拷貝只複製成員變數的值

@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,意味著他們雖然他兩是不同的物件,但是當一個物件改變了任何原有的屬性值是,也會影響另一個物件。

@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());
}
複製程式碼

5. 深拷貝 deep copy

深拷貝就是為了解決淺拷貝的問題。它的優勢在於至少每個可變物件是遞迴拷貝的。 既然深拷貝不依賴於任何可變物件,它將不會被原始拷貝所影響。 下面我們將看到幾種深拷貝的實現方式和他們的優勢

5.1 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()));
}
複製程式碼

In the above implementation of the deep copy, we haven’t created new Strings in our copy constructor because String is an immutable class.

As a result, they can’t be modified by accident. Let’s see if this works:

@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 Interface

The second implementation is based on the clone method inherited from Object. It’s protected, but we need to override it as public.

We’ll also add a marker interface, Cloneable, to the classes to indicate that the classes are actually cloneable.

Let’s add the clone() method to the Address class:

@Override
public Object clone() {
    try {
        return (Address) super.clone();
    } catch (CloneNotSupportedException e) {
        return new Address(this.street, this.getCity(), this.getCountry());
    }
}
複製程式碼

And now let’s implement clone() for the User class:

@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;
}
複製程式碼

Note that the super.clone() call returns a shallow copy of an object, but we set deep copies of mutable fields manually, so the result is correct: super.clone() 返回一個物件的淺拷貝,但是我們可以手動的深拷貝可變成員變數,讓object.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. External Libraries

The above examples look easy, but sometimes they don’t apply as a solution when we can’t add an additional constructor or override the clone method.

This might happen when we don’t own the code, or when the object graph is so complicated that we wouldn’t finish our project on time if we focused on writing additional constructors or implementing the clone method on all classes in the object graph.

What then? In this case, we can use an external library. To achieve a deep copy, we can serialize an object and then deserialize it to a new object.

Let’s look at a few examples.

6.1. Apache Commons Lang

Apache Commons Lang has SerializationUtils#clone, which performs a deep copy when all classes in the object graph implement the Serializable interface.

If the method encounters a class that isn’t serializable, it’ll fail and throw an unchecked SerializationException.

Because of that, we need to add the Serializable interface to our classes:

@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 JSON Serialization with Gson

The other way to serialize is to use JSON serialization. Gson is a library that’s used for converting objects into JSON and vice versa.

Unlike Apache Commons Lang, GSON does not need the Serializable interface to make the conversions.

Let’s have a quick look at an example:

@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. JSON Serialization with Jackson

Jackson is another library that supports JSON serialization. This implementation will be very similar to the one using Gson, but we need to add the default constructor to our classes.

Let’s see an example:

@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. Conclusion

Which implementation should we use when making a deep copy? The final decision will often depend on the classes we’ll copy and whether we own the classes in the object graph.

As always, the complete code samples for this tutorial can be found over on Github

相關文章