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 

Gson、Jackson、および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.浅いコピー

浅いコピーとは、あるオブジェクトから別のオブジェクトにフィールドの値のみをコピーするコピーです

@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!= shutdownCopyは、それらが異なるオブジェクトであることを意味しますが、問題は、元のアドレスのプロパティのいずれかを変更すると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())); }

ディープコピーの上の実装では、我々は新しい作成していない文字列のため、当社のコピーコンストラクタに文字列が不変クラスです。

その結果、誤って変更することはできません。これが機能するかどうかを見てみましょう:

@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メソッドをオーバーライドしたりできない場合、ソリューションとして適用されないことがあります

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

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

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

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。JacksonによるJSONシリアル化

Jacksonは、JSONシリアル化をサポートするもう1つのライブラリです。この実装は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.結論

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

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