Javaでオブジェクトのディープコピーを作成する方法

1前書き

Javaでオブジェクトをコピーしたい場合は、シャローコピーとディープコピーの2つの方法で検討する必要があります。

シャローコピーは、フィールド値のみをコピーする場合のアプローチであるため、コピーは元のオブジェクトに依存している可能性があります。ディープコピーアプローチでは、ツリー内のすべてのオブジェクトが確実に深くコピーされるようにします。そのため、コピーは以前に変更された可能性のある既存のオブジェクトに依存しません。

この記事では、これら2つのアプローチを比較し、ディープコピーを実装するための4つの方法を学習します。

2 Mavenのセットアップ

ディープコピーを実行するさまざまな方法をテストするために、3つのMaven依存関係(Gson、Jackson、およびApache Commons Lang)を使用します。

これらの依存関係を 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>

Gson の最新バージョン、 Jackson 、およびhttps ://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22commons-lang%22%20AND%20a%3A%22commons-lang%22[Apache Commons Lang]はMaven Centralで見つけることができます。 。

3モデル

異なるメソッドを比較してJavaオブジェクトをコピーするには、2つのクラスが必要です。

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
}

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__アドレスにも影響することです。

Address が不変の場合は気にしないでくださいが、そうではありません。

@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ディープコピー

ディープコピーは、この問題を解決するための代替手段です。その利点は、少なくともオブジェクトグラフ内の各可変オブジェクトが再帰的にコピーされることです。

コピーは、以前に作成された変更可能なオブジェクトに依存していないため、シャローコピーで見たように誤って変更されることはありません。

次のセクションでは、いくつかのディープコピーの実装を示し、この利点を説明します。

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 は不変クラスなので、コピーコンストラクタに新しい Strings を作成していません。

結果として、それらは偶然に変更されることはありません。これがうまくいくかどうか見てみましょう:

@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. 複製可能インターフェース

2番目の実装は、 Object から継承されたcloneメソッドに基づいています。保護されていますが、 public として上書きする必要があります。

クラスに実際に複製可能であることを示すために、クラスにマーカーインタフェース 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.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. 外部ライブラリ

上記の例は簡単に見えますが、コンストラクタを追加したりcloneメソッドをオーバーライドしたりできない場合は、解決策として適用されないことがあります。

これは、コードを所有していない場合、またはオブジェクトグラフが非常に複雑なために追加のコンストラクタを作成したり、オブジェクトグラフのすべてのクラスに clone メソッドを実装したりする場合に間に合うようにプロジェクトを完成できない場合に発生します。

それで何?この場合、外部ライブラリを使うことができます。ディープコピーを実現するために、オブジェクトをシリアライズしてから、それを新しいオブジェクトにデシリアライズすることができます。

いくつかの例を見てみましょう。

6.1. Apache Commons Lang

Apache Commons Langには SerializationUtils#cloneがあります。これは、オブジェクトグラフ内のすべてのクラスが Serializable__インターフェースを実装したときにディープコピーを実行します。

  • メソッドが直列化不可能なクラスに遭遇した場合、それは失敗し、チェックされていない SerializationException をスローします。**

そのため、クラスに 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());
}

6.2. GsonによるJSONシリアライゼーション

直列化するもう1つの方法は、JSON直列化を使用することです。 Gsonは、オブジェクトをJSONに変換したり、その逆を行うために使用されるライブラリです。

Apache Commons Langとは異なり、 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はJSONシリアライゼーションをサポートするもう一つのライブラリです。この実装はGsonを使った実装と非常によく似ていますが、 デフォルトコンストラクタをクラスに追加する必要があります

例を見てみましょう。

@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. 結論

ディープコピーを作成するときにどの実装を使用するべきですか?最終的な決定は、コピーするクラスと、オブジェクトグラフ内のクラスを所有しているかどうかによって異なります。

いつものように、このチュートリアルのための完全なコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang-oop[Githubでオーバー]を見つけることができます。