Cloning : ตื้นหรือลึก ?


คิด : cloning คืออะไรครับพ่อ งง ?

พ่อ : ถ้าถามเกี่ยวกับเมท็อด clone ในจาวาล่ะก็  cloning ก็คือการสร้างออปเจกต์ใหม่ที่มีข้อมูลภายใน "เหมือน" กับออปเจกต์ต้นฉบับ เช่น
 

ArrayList x = new ArrayList();

x.add(
"A");
x.add(
"B");
x.add(
"C");
ArrayList y = (ArrayList) x.clone( );
System.out.println(x.equals(y) + ", " + (x==y) );


คำว่า "เหมือน" ในที่นี้ ก็คือการที่เมื่อนำมาผ่าน equals จะได้ค่าจริง โดยต้องไม่ใช่ออปเจกต์เดียวกัน ดังนั้นจากตัวอย่างข้างบน จะแสดง true, false
 


คิด : อยากรู้ครับว่าเมื่อเรียก clone แล้ว  จริงๆ มันเกิดอะไรขึ้น แล้วเราจะเขียนเมท็อดนี้ในคลาสของเราได้อย่างไร ?

พ่อ : จาวาให้บริการ clone ที่คลาส Object ซึ่งเป็น root class โดยทั่วไปเมื่อเราเรียก clone( ) ที่ออปเจกต์หนึ่ง  clone ที่ออปเจกต์นั้นก็จะเรียก super.clone( ) ต่อ ถ้าเรียกทำนองนี้ไปเรื่อยๆ ก็จะต้องถึง clone ของคลาส Object ซึ่งเขากจะสร้าง object ใหม่ที่มีขนาดและเนื้อในเหมือนต้นฉบับ ตัวอย่างเช่น ถ้าคลาส Me มี field 3 ตัวดังแสดงข้างล่างนี้

01: import java.util.ArrayList;
02:
03: public class Me implements Cloneable {
04: ArrayList[] theArray;
05: int x;
06: int y;
07: public Me() {
08: theArray = new ArrayList[20];
09: for (int i = 0; i < theArray.length; i++) {
10: theArray[i] = new ArrayList();
11: }
12: }
13: public Object clone() {
14: try {
15: return super.clone();
16: } catch (CloneNotSupportedException e) {
17: throw new InternalError("can't clone");
18: }
19: }
20:
21: public static void main(String[] args) {
22: Me a = new Me();
23: Me b = (Me) a.clone();
24: System.out.println(a.theArray == b.theArray);
25: }
26: }

เมท็อด clone ข้างบนนี้อาศัยการเรียก super.clone( ) แล้วก็คืนผลที่ได้จาก super.clone( ) สิ่งที่ได้จาก super.clone( ) ซึ่งก็คือที่ได้จาก clone ของคลาส Object ซึ่ง'ก็คือออปเจกต์ใหม่ของคลาส Me โดยมีการจองเนื้อที่และค่าของข้อมูลของ theArray, x และ y เหมือนกัน

ให้สังเกตุว่า theArray เป็น object reference ไปยัง array ดังนั้น theArray ของต้นฉบับ และของที่ได้จาก clone มีค่าเหมือน จึงอ้างอิง array เดียวกัน ดังนั้น ถ้า main ของ Me ถูกเรียกใช้งานจะแสดงค่า true ซึ่งหมายความว่า a.theArray และ b.theArray นั้นอ้างอิงอาเรย์เดียวกัน ดังรูปข้างล่างนี้


คิด : ว้า แล้วทำไมคลาส Object เขาไม่สร้างอาเรย์ให้กับ theArray ใหม่ที่มีค่าเหมือน theArray ของต้นฉบับเลยล่ะ

พ่อ : อ๋อ clone ของ Object เขาทำให้เฉพาะ fields ต่างๆ ที่ปรากฏในออปเจกต์ที่ถูก cloned เท่านั้น  ถ้าอยากทำอย่างที่บอก ก็ต้องเขียนเองเพิ่มเติมใน Me ดังตัวอย่างข้างล่างนี้

01: import java.util.ArrayList;

02:
03: public class Me implements Cloneable {
04: ArrayList[] theArray;
05: int x;
06: int y;
07: public Me() {
08: theArray = new ArrayList[20];
09: for (int i = 0; i < theArray.length; i++) {
10: theArray[i] = new ArrayList();
11: }
12: }
13: public Object clone() {
14: try {
15: Me c = (Me) super.clone();
16: c.theArray = (ArrayList[]) this.theArray.clone();
17: return c;
18: } catch (CloneNotSupportedException e) {
19: throw new InternalError("can't clone");
20: }
21: }
22:
23: public static void main(String[] args) {
24: Me a = new Me();
25: Me b = (Me) a.clone();
26: System.out.println(a.theArray == b.theArray);
27: }
28: }

บรรทัดที่ 16 ข้างบนนี้ จัดการ clone theArray ให้ ดังนั้นถ้าสั่งให้ main ทำงาน คราวนี้ก็จะแสดง false  แล้วผลที่ได้จากการเรียก b = a.clone( ) ก็จะได้ดังรูปข้างล่างนี้

ถ้าดู code ข้างบนตรง constructor (บรรทัดที่ 9-11) จะเห็นว่า theArray นั้นคืออาเรย์จำนวน 20 ช่อง มีไว้เก็บ ออปเจกต์ของ ArrayList  ผลจากการ clone จะได้ b.theArray ที่มีขนาดและข้อมูลภายในเหมือนกัน a.theArray เลย  นั่นก็หมายความว่า ช่องต่างๆ ของ a.theArray อ้างอิงออปเจกต์อะไร ช่องต่างๆ ของ b.theArray ก็อ้างอิงออปเจกต์เหล่านั้น

ลองเพิ่มคำสั่ง System.out.println(a.theArray[0] == b.theArray[0]) ดูก็จะพบว่าคำสั่งนี้แสดงค่า true แสดงว่าเป็น ArrayList เดียวกัน


คิด : แล้วทำไมเขาไม่ clone ออปเจกต์ของ ArrayList ที่เก็บในช่องต่างๆ ของ theArray ด้วยล่ะพ่อ จะได้เป็นของใครของมันก็สิ้นเรื่อง มาอ้างอิงร่วมๆ กันทำไม ?

พ่อ : ก็บอกแล้วไงว่าเขาไม่ทำให้ อยากทำก็ต้องทำเองในเมท็อด clone ดังตัวอย่างข้างล่างนี้

13:   public Object clone() {

14: try {
15: Me c = (Me) super.clone();
16: c.theArray = (ArrayList[]) this.theArray.clone();
17: for (int i = 0; i < theArray.length; i++) {
18: c.theArray[i] = (ArrayList) this.theArray[i].clone();
19: }
20: return c;
21: } catch (CloneNotSupportedException e) {
22: throw new InternalError("can't clone");
23: }
24: }

บรรทัดที่ 17 ถึง 19 เข้าไปเพื่อทำการ clone ออปเจกต์ของทุกๆ ช่องในอาเรย์เลย

คราวนี้ถ้ามีการเรียก b = a.clone( ) ก็จะได้ผลดังรูปข้างล่างนี้


คิด : แล้วแต่ละ arraylist นั้นก็คงมีการอ้างอิงออปเจกต์อื่นๆ อีก จริงไหมพ่อ ? เขา clone ให้ด้วยหรือเปล่า ?

พ่อ : เอ๊ะจะต้องให้บอกกี่ครั้ง ว่า clone ของคลาส Object เขา clone ให้แค่เฉพาะ fields ต่างๆ ของออปเจกต์ที่ถูก cloned เท่านั้น   เรา clone อาเรย์ เขาก็ clone ให้เฉพาะตัวอาเรย์ ไม่ได้ clone ออปเจกต์ต่างๆ ที่อาเรย์นั้นอ้างอิง  การ clone ออปเจกต์ของ ArrayList นั้น ผู้เขียน ArrayList เขาก็ clone ให้ "ตื้นๆ" คือเขา clone ให้เฉพาะออปเจกต์ ArrayList ไม่ได้ clone ออปเจกต์อื่นๆ ใน list  คนทั่วไปที่เขาเขียน clone ก็มัก clone ให้ "ตื้น" ๆ แบบนี้แหละ เรียกกันว่า shallow cloning   แต่ถ้าเราจะให้บริการ clone แบบ "ลึกๆ" ที่เรียกกันว่า deep cloning ก็เป็นสิทธิ์ที่ทำได้  สำหรับกรณีคลาส Me ที่เขียนแบบแรก ก็ถือว่าตื้นที่สุด แบบที่สองก็ลึกหน่อย (มีการ clone อาเรย์ด้วย) และแบบสุดท้าย ก็ถือได้ว่าลึก เพราะเรา clone แต่ละ arraylist ต่อ

clone ที่เราเขียนจะลึกจะตื้นเท่าใด ก็ขึ้นกับลักษณะของคลาส และการตัดสินใจในการออกแบบคลาส


คิด : ไม่ถามแล้วครับ ไปเล่นเกมส์ดีกว่า


เกร็ดกาแฟ

Copyright 2003 Somchai Prasitjutrakul