ข้อดีอย่างหนึ่งของภาษา Java คือการยกเลิกแนวคิดของพอยน์เตอร์ แต่มันก็ทำให้โปรแกรมเมอร์จำนวนมากมักจะเพิกเฉยต่อความแตกต่างระหว่างวัตถุและการอ้างอิงในการเขียนโปรแกรม บทความนี้จะพยายามชี้แจงแนวคิดนี้ และเนื่องจาก Java ไม่สามารถแก้ปัญหาการคัดลอกวัตถุผ่านการกำหนดอย่างง่ายในระหว่างกระบวนการพัฒนาจึงจำเป็นต้องใช้วิธีการโคลน () เพื่อคัดลอกวัตถุ บทความนี้จะช่วยให้คุณเข้าใจว่าโคลนเงาและโคลนลึกคืออะไรและเข้าใจความแตกต่างข้อดีและข้อเสียของพวกเขา
คุณสับสนเล็กน้อยเมื่อคุณเห็นชื่อนี้: ภาษา Java ระบุอย่างชัดเจนว่าตัวชี้ถูกยกเลิกเพราะพอยน์เตอร์มักจะสะดวกและยังเป็นสาเหตุของความไม่มั่นคงของรหัสและทำให้โปรแกรมซับซ้อนและเข้าใจยากมาก รหัสที่เขียนโดยการใช้พอยน์เตอร์ในทางที่ผิดนั้นไม่น้อยไปกว่าการใช้คำสั่ง "goto" ที่น่าอับอายอยู่แล้ว มันเป็นการดีที่ Java จะยอมแพ้แนวคิดของพอยน์เตอร์ แต่นี่เป็นเพียงว่าไม่มีคำจำกัดความตัวชี้ที่ชัดเจนในภาษา Java ในสาระสำคัญทุกคำสั่งใหม่จะส่งคืนการอ้างอิงตัวชี้ อย่างไรก็ตามในกรณีส่วนใหญ่ Java ไม่จำเป็นต้องกังวลเกี่ยวกับวิธีการใช้งาน "ตัวชี้" นี้ให้คนเดียวกลัวเหมือนเมื่อพอยน์เตอร์ปฏิบัติการใน C ++ สิ่งเดียวที่ต้องใส่ใจมากขึ้นคือเมื่อส่งวัตถุไปยังฟังก์ชั่น
แพ็คเกจ com.zoer.src; คลาสสาธารณะ objref {obj aobj = new obj (); int aint = 11; โมฆะสาธารณะ ChangeObj (obj inobj) {inobj.str = "เปลี่ยนค่า"; } โมฆะสาธารณะ ChangePRI (int inint) {inint = 22; } โมฆะคงที่สาธารณะหลัก (สตริง [] args) {objref oref = new objref (); System.out.println ("ก่อนการเรียกใช้วิธีการเปลี่ยนแปลง ():" + oref.aobj); oref.changeobj (oref.aobj); System.out.println ("หลังจากการโทร CHANGEOBJ ():" + oref.aobj); System.out.println ("================================================================================================================================== - - - วิธีการโทรเปลี่ยน (): " + oref.aint); - แพ็คเกจ com.zoer.src; คลาสสาธารณะ obj {string str = "ค่าเริ่มต้น"; สตริงสาธารณะ toString () {return str; - ส่วนหลักของรหัสนี้เรียกใช้สองวิธีที่คล้ายกันมากคือการเปลี่ยนแปลง () และการเปลี่ยนแปลง () ความแตกต่างเพียงอย่างเดียวคือหนึ่งใช้วัตถุเป็นพารามิเตอร์อินพุตและอีกอันใช้ประเภทพื้นฐาน int ใน Java เป็นพารามิเตอร์อินพุต และพารามิเตอร์อินพุตจะเปลี่ยนไปภายในทั้งสองส่วนฟังก์ชั่น วิธีการที่ดูเหมือนกัน แต่ผลลัพธ์ผลลัพธ์ของโปรแกรมแตกต่างกัน วิธีการเปลี่ยนแปลง () เปลี่ยนพารามิเตอร์อินพุตจริง ๆ ในขณะที่วิธีการเปลี่ยนแปลง () ไม่ได้เปลี่ยนพารามิเตอร์อินพุต
จากตัวอย่างนี้ Java ประมวลผลวัตถุและประเภทข้อมูลพื้นฐานแตกต่างกัน เช่นเดียวกับ C เมื่อชนิดข้อมูลพื้นฐานของ Java (เช่น Int, Char, Double, ฯลฯ ) จะถูกส่งผ่านไปยังฟังก์ชั่น Body เป็นพารามิเตอร์รายการพารามิเตอร์ที่ผ่านจะกลายเป็นตัวแปรท้องถิ่นภายในตัวถังฟังก์ชั่น ตัวแปรโลคัลนี้เป็นสำเนาของพารามิเตอร์อินพุต การดำเนินการทั้งหมดภายในฟังก์ชั่นนี้เป็นการดำเนินการสำหรับสำเนานี้ หลังจากการดำเนินการฟังก์ชั่นเสร็จสิ้นตัวแปรท้องถิ่นจะเสร็จสิ้นภารกิจของตนและจะไม่ส่งผลกระทบต่อตัวแปรเป็นพารามิเตอร์อินพุต วิธีการผ่านพารามิเตอร์นี้เรียกว่า "การส่งผ่าน" ใน Java การผ่านโดยใช้วัตถุเป็นพารามิเตอร์รายการคือ "การอ้างอิงผ่าน" ซึ่งหมายความว่ามีเพียง "การอ้างอิง" ของวัตถุเท่านั้นที่ผ่าน แนวคิดของ "การอ้างอิง" นี้เหมือนกับการอ้างอิงตัวชี้ในภาษา C เมื่อตัวแปรอินพุตเปลี่ยนไปภายในร่างกายของฟังก์ชั่นมันจะเป็นการดำเนินการโดยตรงกับวัตถุ
ยกเว้น "การอ้างอิงผ่าน" เมื่อผ่านค่าของฟังก์ชัน "การอ้างอิงผ่าน" เมื่อกำหนดค่าให้กับตัวแปรวัตถุโดยใช้ "=" มันคล้ายกับการให้ตัวแปรนามแฝงอื่น ชื่อทั้งสองชี้ไปที่วัตถุเดียวกันในหน่วยความจำ
ในการเขียนโปรแกรมจริงเรามักจะพบกับสถานการณ์นี้: มีวัตถุ A ที่มีค่าที่ถูกต้องในช่วงเวลาหนึ่ง ในเวลานี้วัตถุ B ใหม่อาจจำเป็นเช่นเดียวกับ A และการเปลี่ยนแปลงใด ๆ ที่เป็น B จะไม่ส่งผลกระทบต่อค่าใน A ซึ่งคือ A และ B เป็นวัตถุอิสระสองชิ้น แต่ค่าเริ่มต้นของ B ถูกกำหนดโดยวัตถุ ในภาษา Java การใช้คำสั่งการมอบหมายอย่างง่ายไม่สามารถตอบสนองความต้องการนี้ได้ แม้ว่าจะมีหลายวิธีในการตอบสนองความต้องการนี้ แต่วิธีที่ง่ายที่สุดและมีประสิทธิภาพมากที่สุดในการใช้วิธีการโคลน () นั้นอยู่ในหมู่พวกเขา
คลาส Java ทั้งหมดสืบทอดคลาส Java.lang.Object โดยค่าเริ่มต้นและมีวิธีการโคลน () ในคลาส Java.lang.Object เอกสาร JDK API อธิบายวิธีนี้จะส่งคืนสำเนาของวัตถุวัตถุ มีสองจุดที่จะอธิบาย: ก่อนอื่นวัตถุคัดลอกส่งคืนวัตถุใหม่ไม่ใช่การอ้างอิง ประการที่สองความแตกต่างระหว่างการคัดลอกวัตถุและวัตถุใหม่ที่ส่งคืนด้วยตัวดำเนินการใหม่คือสำเนานี้มีข้อมูลบางอย่างเกี่ยวกับวัตถุต้นฉบับมากกว่าข้อมูลเริ่มต้นของวัตถุ
วิธีใช้วิธีการโคลน ()
รหัสการโทรไปยังโคลน () โดยทั่วไปมีดังนี้:
คลาสสาธารณะ Cloneclass ใช้ cloneable {public int aint; object clone () {cloneclass o = null; ลอง {o = (cloneclass) super.clone (); } catch (clonenotsupportedException e) {e.printStackTrace (); } return o; - มีสามคะแนนที่น่าสังเกต ขั้นแรกคลาส Cloneclass ที่หวังว่าจะใช้ฟังก์ชั่นโคลนใช้อินเทอร์เฟซ cloneable อินเทอร์เฟซนี้เป็นของแพ็คเกจ java.lang แพ็คเกจ java.lang ถูกนำเข้าสู่คลาสเริ่มต้นดังนั้นจึงไม่จำเป็นต้องเขียนเป็น java.lang.cloneable อีกสิ่งหนึ่งที่น่าสังเกตคือวิธีการโคลน () มากเกินไป ในที่สุด super.clone () ถูกเรียกในเมธอด clone () ซึ่งหมายความว่าไม่ว่าโครงสร้างการสืบทอดของคลาสโคลนจะเป็นอย่างไร super.clone () โดยตรงหรือโดยอ้อมเรียกวิธีการโคลน () ของคลาส java.lang.object มาอธิบายจุดเหล่านี้โดยละเอียดด้านล่าง
ควรกล่าวว่าจุดที่สามเป็นสิ่งสำคัญที่สุด หากคุณสังเกตวิธีการดั้งเดิมของ clone clone () อย่างรอบคอบประสิทธิภาพของวิธีการดั้งเดิมโดยทั่วไปจะสูงกว่าวิธีการที่ไม่ใช่เจ้าของภาษาใน Java สิ่งนี้ยังอธิบายว่าทำไมเราควรใช้เมธอด clone () ในวัตถุแทนที่จะเป็นคลาส A ใหม่แรกจากนั้นกำหนดข้อมูลจากวัตถุต้นฉบับให้กับวัตถุใหม่แม้ว่าสิ่งนี้จะใช้ฟังก์ชันโคลน สำหรับจุดที่สองเราควรสังเกตว่า clone () ในคลาสวัตถุเป็นวิธีที่มีคุณสมบัติที่ได้รับการป้องกันหรือไม่ นอกจากนี้ยังหมายความว่าหากคุณต้องการใช้วิธีการโคลน () คุณต้องสืบทอดคลาสวัตถุ ใน Java คลาสทั้งหมดสืบทอดคลาส Object โดยค่าเริ่มต้นดังนั้นคุณไม่ต้องกังวลเกี่ยวกับเรื่องนี้ จากนั้นวิธีการ overload clone () อีกสิ่งที่ต้องพิจารณาคือเพื่อให้คลาสอื่นเรียกวิธีการโคลน () ของคลาสโคลนนี้หลังจากการโอเวอร์โหลดคุณลักษณะของวิธีการโคลน () จะต้องตั้งค่าเป็นสาธารณะ
เหตุใดคลาสโคลนจึงยังคงใช้อินเทอร์เฟซ cloneable โปรดทราบว่าอินเทอร์เฟซ cloneable ไม่มีวิธีใด ๆ ! ในความเป็นจริงอินเทอร์เฟซนี้เป็นเพียงธงและธงนี้มีไว้สำหรับวิธี clone () ในคลาสวัตถุ หากคลาสโคลนไม่ได้ใช้อินเทอร์เฟซ cloneable และเรียกใช้วิธีการโคลน () ของวัตถุ (นั่นคือวิธี super.clone () เรียกว่า) แล้ววิธีการโคลน () ของวัตถุจะโยนข้อยกเว้น clonenotsupportedexception
ข้างต้นเป็นขั้นตอนพื้นฐานที่สุดของโคลน หากคุณต้องการทำโคลนที่ประสบความสำเร็จให้สำเร็จคุณต้องเข้าใจว่า "Shadow Clone" และ "Deep Clone" คืออะไร
Shadow Clone คืออะไร?
แพ็คเกจ com.zoer.src; ชั้นเรียน UNCLONEA {Private int i; สาธารณะ unclonea (int ii) {i = ii; } โมฆะสาธารณะ doublevalue () {i *= 2; } สตริงสาธารณะ toString () {return integer.toString (i); }} คลาส CloneB ใช้ cloneable {public int aint; สาธารณะ UNCLONEA UNCA = ใหม่ UNCLONEA (111); วัตถุสาธารณะโคลน () {cloneb o = null; ลอง {o = (cloneb) super.clone (); } catch (clonenotsupportedException e) {e.printStackTrace (); } return o; }} คลาสสาธารณะ objref {โมฆะคงที่สาธารณะหลัก (สตริง [] a) {cloneb b1 = new cloneb (); b1.aint = 11; System.out.println ("ก่อนโคลน, b1.aint =" + b1.aint); System.out.println ("ก่อนโคลน, b1.unca =" + b1.unca); cloneb b2 = (cloneb) b1.clone (); b2.aint = 22; b2.unca.doublevalue (); System.out.println ("======================================================================================================================== - - - - - - โคลน, b2.aint = " + b2.aint); System.out.println ("หลังจากโคลน, b2.unca =" + b2.unca); - ผลลัพธ์ผลลัพธ์:
ก่อนโคลน b1.aint = 11before clone, b1.unca = 111 ================================================================================= - - - - - - -
ผลลัพธ์ผลลัพธ์แสดงให้เห็นว่าตัวแปร int AINT และผลลัพธ์การโคลนของวัตถุอินสแตนซ์ unca ของ unclonea นั้นไม่สอดคล้องกัน ประเภท int เป็นโคลนอย่างแท้จริงเพราะตัวแปร AINT ใน B2 ไม่มีผลต่อ AINT ของ B1 กล่าวคือ B2.aint และ B1.aint ครอบครองพื้นที่หน่วยความจำที่แตกต่างกันแล้ว B2.aint เป็นสำเนา B1.aint ที่แท้จริง ในทางตรงกันข้ามการเปลี่ยนแปลงเป็น b2.unca เปลี่ยนไปพร้อมกันเป็น b1.unca และเป็นที่ชัดเจนว่า b2.unca และ b1.unca เป็นข้อมูลอ้างอิงที่แตกต่างกันซึ่งชี้ไปที่วัตถุเดียวกันเท่านั้น! จากนี้เราจะเห็นได้ว่าเอฟเฟกต์ของการเรียกเมธอด clone () ในคลาสวัตถุคือ: ก่อนเปิดชิ้นส่วนของพื้นที่ในหน่วยความจำที่เหมือนกับวัตถุต้นฉบับแล้วคัดลอกเนื้อหาในวัตถุดั้งเดิมตามที่เป็นอยู่ สำหรับประเภทข้อมูลพื้นฐานการดำเนินการดังกล่าวไม่ได้เป็นปัญหา แต่สำหรับตัวแปรประเภทที่ไม่ได้ใช้งานเรารู้ว่าพวกเขาบันทึกการอ้างอิงไปยังวัตถุเท่านั้นซึ่งยังทำให้ตัวแปรประเภทที่ไม่ได้ใช้งานหลังจากโคลนชี้ไปที่วัตถุเดียวกันและตัวแปรที่เกี่ยวข้องในวัตถุดั้งเดิม
ส่วนใหญ่แล้วผลลัพธ์ของโคลนนี้มักจะไม่ใช่ผลลัพธ์ที่เราหวังไว้และโคลนนี้เรียกว่า "Shadow Clone" ในการทำให้ B2.unca ชี้ไปที่วัตถุที่แตกต่างจาก b2.unca และ b2.unca ยังมีข้อมูลใน B1.unca เป็นข้อมูลเริ่มต้นคุณต้องใช้โคลนเชิงลึก
วิธีการทำโคลนลึก?
มันง่ายมากที่จะเปลี่ยนตัวอย่างข้างต้นเป็นโคลนลึกและจำเป็นต้องมีการเปลี่ยนแปลงสองครั้ง: หนึ่งคือการอนุญาตให้คลาส UNCLONEA ใช้ฟังก์ชั่นโคลนเดียวกันกับคลาส ClONEB (ใช้อินเตอร์เฟส cloneable และวิธีการโคลน () มากเกินไป) ประการที่สองคือการเพิ่มประโยค o.unca = (unclonea) unca.clone () ลงในวิธี clone () ของ cloneb;
แพ็คเกจ com.zoer.src; ชั้นเรียน UNCLONEA ใช้ cloneable {private int i; สาธารณะ unclonea (int ii) {i = ii; } โมฆะสาธารณะ doublevalue () {i *= 2; } สตริงสาธารณะ toString () {return integer.toString (i); } object clone () {unclonea o = null; ลอง {o = (unclonea) super.clone (); } catch (clonenotsupportedException e) {e.printStackTrace (); } return o; }} คลาส CloneB ใช้ cloneable {public int aint; สาธารณะ UNCLONEA UNCA = ใหม่ UNCLONEA (111); วัตถุสาธารณะโคลน () {cloneb o = null; ลอง {o = (cloneb) super.clone (); } catch (clonenotsupportedException e) {e.printStackTrace (); } o.unca = (unclonea) unca.clone (); กลับมา; }} คลาสสาธารณะ clonemain {โมฆะคงที่สาธารณะหลัก (สตริง [] a) {cloneb b1 = new cloneb (); b1.aint = 11; System.out.println ("ก่อนโคลน, b1.aint =" + b1.aint); System.out.println ("ก่อนโคลน, b1.unca =" + b1.unca); cloneb b2 = (cloneb) b1.clone (); b2.aint = 22; b2.unca.doublevalue (); System.out.println ("==================================================================================== - - - โคลน, b1.aint = " + b1.aint); System.out.println ("หลังจากโคลน, b1.unca =" + b1.unca); System.out.println ("=========================================================================================================================== - - - - - - ผลลัพธ์ผลลัพธ์:
ก่อนโคลน b1.aint = 11before clone, b1.unca = 111 ================================================================================= - - - - - - -
จะเห็นได้ว่าการเปลี่ยนแปลงปัจจุบันของ B2.unca ไม่มีผลต่อ B1.unca ในเวลานี้ b1.unca และ b2.unca ชี้ไปที่สองอินสแตนซ์ที่แตกต่างกันและ b1 และ b2 มีค่าเดียวกันในขณะที่ cloneb b2 = (cloneb) b1.clone (); เรียกว่าที่นี่ b1.i = b2.i = 11
คุณควรรู้ว่าไม่ใช่ทุกคลาสที่สามารถใช้โคลนลึก ตัวอย่างเช่นหากคุณเปลี่ยนตัวแปรประเภท UNCLONEA ในคลาส ClONEB ด้านบนเป็นประเภท StringBuffer ให้ดูคำอธิบายเกี่ยวกับ StringBuffer ใน JDK API StringBuffer ไม่ได้โอเวอร์โหลดเมธอด clone () และสิ่งที่ร้ายแรงกว่าคือ StringBuffer ยังคงเป็นคลาสสุดท้ายซึ่งหมายความว่าเราไม่สามารถใช้โคลนของ StringBuffer ทางอ้อมโดยใช้การสืบทอด หากคลาสมีวัตถุประเภท StringBuffer หรือวัตถุที่คล้ายกับ StringBuffer เรามีสองตัวเลือก: อาจใช้โคลนเงาเท่านั้นหรือเพิ่มประโยคลงในวิธีการโคลน () ของคลาส (สมมติว่าเป็นวัตถุ Sringbuffer // อันดั้งเดิมคือ: o.unca = (unclonea) unca.clone ();
ควรสังเกตว่านอกเหนือจากประเภทข้อมูลพื้นฐานที่สามารถใช้โคลนลึกโดยอัตโนมัติวัตถุสตริงเป็นข้อยกเว้น ประสิทธิภาพของมันหลังจากโคลนดูเหมือนจะใช้โคลนลึก แม้ว่านี่จะเป็นเพียงภาพลวงตา แต่ก็ช่วยอำนวยความสะดวกในการเขียนโปรแกรมของเราอย่างมาก
ความแตกต่างระหว่างสตริงและสตริงบัฟฟ์ในโคลนควรอธิบายว่านี่ไม่ได้มุ่งเน้นไปที่ความแตกต่างระหว่างสตริงและสตริงบัฟเฟอร์ แต่จากตัวอย่างนี้เรายังสามารถเห็นคุณสมบัติที่ไม่ซ้ำกันของคลาสสตริง
ตัวอย่างต่อไปนี้มีสองคลาส คลาส Clonec มีตัวแปรประเภทสตริงและตัวแปรประเภท StringBuffer และใช้วิธี clone () ตัวแปรประเภท Clonec C1 ถูกประกาศในคลาส StrClone จากนั้นวิธีการ clone () ของ C1 จะถูกเรียกให้สร้างสำเนาของ C1 C2 หลังจากเปลี่ยนตัวแปรประเภทสตริงและสตริงบัฟเฟอร์ใน C2 ผลลัพธ์จะถูกพิมพ์:
แพ็คเกจ com.zoer.src; Class Clonec ใช้ cloneable {public string str; Stringbuffer สาธารณะ Strbuff; object clone () {clonec o = null; ลอง {o = (clonec) super.clone (); } catch (clonenotsupportedException e) {e.printStackTrace (); } return o; }} คลาสสาธารณะ strclone {โมฆะคงที่สาธารณะหลัก (สตริง [] a) {clonec c1 = new clonec (); c1.str = สตริงใหม่ ("initializestr"); c1.strbuff = new Stringbuffer ("Initializestrbuff"); System.out.println ("ก่อนโคลน, c1.str =" + c1.str); System.out.println ("ก่อนโคลน, c1.strbuff =" + c1.strbuff); clonec c2 = (clonec) c1.clone (); c2.str = c2.str.substring (0, 5); c2.strbuff = c2.strbuff.append ("เปลี่ยน strbuff clone"); System.out.println ("================================================== - - - - - - - System.out.println ("หลังจากโคลน, c2.strbuff =" + c2.strbuff);}} ผลการดำเนินการ:
<span style = "font-family: 'Microsoft Yahei';"> <span style = "ตัวอักษรขนาด: 16px;"> ก่อนโคลน c1.str = initializestr - - - - Clone, c2.str = Initializestrbuff เปลี่ยน strbuff clone </span> </span>
ผลการพิมพ์แสดงให้เห็นว่าตัวแปรของประเภทสตริงดูเหมือนจะใช้โคลนเชิงลึกเนื่องจากการเปลี่ยนแปลงเป็น C2.STR ไม่ได้ส่งผลกระทบต่อ C1.STR! Java ถือว่าคลาส Sring เป็นประเภทข้อมูลพื้นฐานหรือไม่? ที่จริงแล้วนี่ไม่ใช่กรณี มีเคล็ดลับเล็ก ๆ ที่นี่ ความลับอยู่ในคำสั่ง c2.str = c2.str.substring (0,5)! ในสาระสำคัญ c1.str และ c2.str ยังคงอ้างอิงเมื่อโคลนและทั้งสองชี้ไปที่วัตถุสตริงเดียวกัน แต่เมื่อ c2.str = c2.str.substring (0,5) มันเทียบเท่ากับการสร้างประเภทสตริงใหม่จากนั้นกำหนดกลับไปที่ C2.str นี่เป็นเพราะสตริงถูกเขียนเป็นคลาสที่ไม่เปลี่ยนรูปโดยวิศวกรดวงอาทิตย์และฟังก์ชั่นในคลาสสตริงทั้งหมดไม่สามารถเปลี่ยนค่าของตนเองได้
ข้างต้นเป็นเรื่องเกี่ยวกับบทความนี้ ฉันหวังว่ามันจะเป็นประโยชน์สำหรับทุกคนที่จะเข้าใจการจำลองแบบลึกและตื้นในชวา