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