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

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

1. 前書き

Javaでオブジェクトをコピーする場合、考慮する必要のある2つの可能性があります。浅いコピーと深いコピーです。

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

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

2. Mavenセットアップ

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

これらの依存関係をpom.xmlに追加しましょう:


    com.google.code.gson
    gson
    2.8.2


    commons-lang
    commons-lang
    2.6


    com.fasterxml.jackson.core
    jackson-databind
    2.9.3

GsonJackson、およびApache Commons Langの最新バージョンは、MavenCentralにあります。

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. 浅いコピー

浅いコピーとは、あるオブジェクトから別のオブジェクトへのwe only copy values of fieldsのコピーです。

@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は、they’re different objects, but the problem is that when we change any of the original address' properties, this will also affect the shallowCopy‘s addressを意味します。

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

ディープコピーは、この問題を解決する代替手段です。 その利点は、少なくともeach mutable object in the object graph is recursively copiedであるということです。

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

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

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. 外部ライブラリ

上記の例は簡単に見えますが、ソリューションwhen we can’t add an additional constructor or override the clone methodとして適用されない場合があります。

これは、コードを所有していない場合、またはオブジェクトグラフが非常に複雑で、追加のコンストラクターの記述や、のすべてのクラスでのcloneメソッドの実装に焦点を合わせた場合にプロジェクトを時間どおりに完了できない場合に発生する可能性があります。オブジェクトグラフ。

それで何? この場合、外部ライブラリを使用できます。 ディープコピーを実現するには、we can serialize an object and then deserialize it to a new object

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

6.1. アパッチコモンズラング

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 does not need the Serializable interface to make the conversions

例を簡単に見てみましょう。

@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を使用した実装と非常に似ていますが、we need to add the default constructor to our classesです。

例を見てみましょう:

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

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

いつものように、このチュートリアルの完全なコードサンプルはover on Githubにあります。