如何對JAVA對象進行深拷貝

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

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

先定義一下Modelgit

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

如下演示淺拷貝只複製成員變量的值github

@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,意味着他們雖然他兩是不一樣的對象,可是當一個對象改變了任何原有的屬性值是,也會影響另外一個對象。bash

@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

深拷貝就是爲了解決淺拷貝的問題。它的優點在於至少每一個可變對象是遞歸拷貝的。 既然深拷貝不依賴於任何可變對象,它將不會被原始拷貝所影響。 下面咱們將看到幾種深拷貝的實現方式和他們的優點app

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.maven

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

@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:ui

@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

相關文章
相關標籤/搜索