Cara Membuat Salinan Dalam dari Objek di Java

1. Perkenalan

Saat kita ingin menyalin objek di Java, ada dua kemungkinan yang perlu kita pertimbangkan - salinan dangkal dan salinan dalam.

Salinan dangkal adalah pendekatan saat kita hanya menyalin nilai bidang dan karena itu salinan mungkin bergantung pada objek aslinya. Dalam pendekatan deep copy, kami memastikan bahwa semua objek di pohon telah disalin secara mendalam, sehingga salinan tersebut tidak bergantung pada objek yang ada sebelumnya yang mungkin pernah berubah.

Pada artikel ini, kita akan membandingkan dua pendekatan ini dan mempelajari empat metode untuk menerapkan salinan dalam.

2. Pengaturan Maven

Kami akan menggunakan tiga dependensi Maven - Gson, Jackson, dan Apache Commons Lang - untuk menguji berbagai cara melakukan deep copy.

Mari tambahkan dependensi ini ke pom.xml kita :

 com.google.code.gson gson 2.8.2   commons-lang commons-lang 2.6   com.fasterxml.jackson.core jackson-databind 2.9.3 

Versi terbaru Gson, Jackson, dan Apache Commons Lang dapat ditemukan di Maven Central.

3. Model

Untuk membandingkan metode yang berbeda untuk menyalin objek Java, kita memerlukan dua kelas untuk dikerjakan:

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. Salinan Dangkal

Salinan dangkal adalah salinan di mana kami hanya menyalin nilai bidang dari satu objek ke objek lainnya:

@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); }

Dalam hal ini pm! = ShallowCopy , yang berarti mereka adalah objek yang berbeda, tetapi masalahnya adalah ketika kita mengubah salah satu properti alamat asli , ini juga akan mempengaruhi alamat shallowCopy .

Kami tidak akan mempermasalahkannya jika Address tidak dapat diubah, tetapi tidak:

@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. Salinan Dalam

Salinan dalam adalah alternatif yang memecahkan masalah ini. Keuntungannya adalah setidaknya setiap objek yang dapat berubah dalam grafik objek disalin secara rekursif .

Karena salinan tidak bergantung pada objek yang dapat berubah apa pun yang dibuat sebelumnya, itu tidak akan dimodifikasi secara tidak sengaja seperti yang kita lihat pada salinan dangkal.

Di bagian berikut, kami akan menunjukkan beberapa implementasi deep copy dan mendemonstrasikan keuntungan ini.

5.1. Salin Pembuat

Implementasi pertama yang akan kami terapkan didasarkan pada konstruktor salinan:

public Address(Address that) { this(that.getStreet(), that.getCity(), that.getCountry()); }
public User(User that) { this(that.getFirstName(), that.getLastName(), new Address(that.getAddress())); }

Dalam implementasi deep copy di atas, kami belum membuat Strings baru di konstruktor salinan kami karena String adalah kelas yang tidak dapat diubah.

Akibatnya, mereka tidak dapat dimodifikasi secara tidak sengaja. Mari kita lihat apakah ini berhasil:

@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. Antarmuka yang Dapat Dikloning

Implementasi kedua didasarkan pada metode klon yang diwarisi dari Object . Ini dilindungi, tapi kita perlu menggantinya sebagai publik .

Kami juga akan menambahkan antarmuka marker, Cloneable, ke kelas untuk menunjukkan bahwa kelas sebenarnya dapat digandakan.

Mari tambahkan metode clone () ke kelas Alamat :

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

Dan sekarang mari kita implementasikan clone () untuk kelas User :

@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; }

Perhatikan bahwa panggilan super.clone () mengembalikan salinan objek yang dangkal, tetapi kami menyetel salinan dalam dari bidang yang bisa berubah secara manual, sehingga hasilnya benar:

@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. Perpustakaan Eksternal

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. Serialisasi JSON Dengan Jackson

Jackson adalah pustaka lain yang mendukung serialisasi JSON. Implementasi ini akan sangat mirip dengan yang menggunakan Gson, tetapi kita perlu menambahkan konstruktor default ke kelas kita .

Mari kita lihat contohnya:

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

Penerapan apa yang harus kita gunakan saat membuat salinan dalam? Keputusan akhir akan sering bergantung pada kelas yang akan kita salin dan apakah kita memiliki kelas dalam grafik objek.

Seperti biasa, contoh kode lengkap untuk tutorial ini dapat ditemukan di GitHub.