วิธีการสร้างสำเนาลึกของวัตถุใน Java

1. บทนำ

เมื่อเราต้องการคัดลอกวัตถุใน Java มีความเป็นไปได้สองประการที่เราต้องพิจารณา - สำเนาตื้นและสำเนาลึก

สำเนาตื้นเป็นแนวทางเมื่อเราคัดลอกเฉพาะค่าฟิลด์ดังนั้นสำเนาจึงอาจขึ้นอยู่กับวัตถุต้นฉบับ ในแนวทางการคัดลอกแบบลึกเราตรวจสอบให้แน่ใจว่าวัตถุทั้งหมดในโครงสร้างถูกคัดลอกอย่างละเอียดดังนั้นสำเนาจึงไม่ขึ้นอยู่กับวัตถุใด ๆ ที่มีอยู่ก่อนหน้านี้ที่อาจเปลี่ยนแปลงไป

ในบทความนี้เราจะเปรียบเทียบสองวิธีนี้และเรียนรู้สี่วิธีในการนำสำเนาแบบลึกไปใช้

2. การตั้งค่า Maven

เราจะใช้การอ้างอิงของ 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 เวอร์ชันล่าสุดมีอยู่ใน Maven Central

3. รุ่น

ในการเปรียบเทียบวิธีต่างๆในการคัดลอกออบเจ็กต์ Java เราจะต้องใช้สองคลาสเพื่อทำงาน:

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

ในกรณีนี้น! = shallowCopyซึ่งหมายความว่าพวกเขากำลังวัตถุที่แตกต่าง แต่ปัญหาก็คือว่าเมื่อเราเปลี่ยนใด ๆ ของเดิมที่อยู่คุณสมบัตินี้จะส่งผลกระทบต่อshallowCopy 's อยู่

เราจะไม่กังวลเกี่ยวกับเรื่องนี้หาก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())); }

ในการใช้งาน Deep Copy ข้างต้นเรายังไม่ได้สร้างStringsใหม่ในตัวสร้างการคัดลอกของเราเนื่องจากStringเป็นคลาสที่ไม่เปลี่ยนรูป

ด้วยเหตุนี้จึงไม่สามารถแก้ไขได้โดยบังเอิญ มาดูกันว่าได้ผลหรือไม่:

@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

การดำเนินงานที่สองจะขึ้นอยู่กับวิธีการโคลนสืบทอดมาจากวัตถุ มันได้รับการคุ้มครอง แต่เราต้องแทนที่มันเป็นที่สาธารณะ

นอกจากนี้เราจะเพิ่มอินเทอร์เฟซเครื่องหมายCloneableให้กับคลาสเพื่อระบุว่าคลาสนั้นสามารถโคลนได้จริง

มาเพิ่มวิธีการclone ()ในคลาสAddress :

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

ตอนนี้มาใช้clone ()สำหรับคลาส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; }

โปรดทราบว่าการเรียก 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. ห้องสมุดภายนอก

ตัวอย่างข้างต้นดูง่าย แต่บางครั้งก็ไม่สามารถใช้เป็นวิธีแก้ปัญหาได้เมื่อเราไม่สามารถเพิ่มตัวสร้างเพิ่มเติมหรือแทนที่วิธีการโคลนได้

สิ่งนี้อาจเกิดขึ้นเมื่อเราไม่ได้เป็นเจ้าของโค้ดหรือเมื่อกราฟออบเจ็กต์มีความซับซ้อนมากจนเราจะทำโปรเจ็กต์ไม่เสร็จทันเวลาหากเรามุ่งเน้นไปที่การเขียนตัวสร้างเพิ่มเติมหรือใช้วิธีการโคลนกับคลาสทั้งหมดในกราฟออบเจ็กต์

แล้วอะไรล่ะ? ในกรณีนี้เราสามารถใช้ไลบรารีภายนอกได้ เพื่อให้บรรลุสำเนาลึกเราสามารถเป็นอันดับวัตถุแล้ว deserialize ไปยังวัตถุใหม่

ลองดูตัวอย่างเล็กน้อย

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 กับ Jackson

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. สรุป

เราควรใช้การดำเนินการใดเมื่อทำสำเนาแบบลึก การตัดสินใจขั้นสุดท้ายมักจะขึ้นอยู่กับคลาสที่เราจะคัดลอกและเราเป็นเจ้าของคลาสในกราฟออบเจ็กต์หรือไม่

เช่นเคยตัวอย่างโค้ดทั้งหมดสำหรับบทช่วยสอนนี้สามารถพบได้ใน GitHub