تتمثل إحدى ميزة لغة Java في أنها تلغي مفهوم المؤشرات ، ولكنها تقود أيضًا العديد من المبرمجين إلى تجاهل الفرق بين الكائنات والمراجع في البرمجة. سيحاول هذا المقال توضيح هذا المفهوم. ولأن Java لا يمكنها حل مشكلة نسخ الكائن من خلال المهمة البسيطة ، أثناء عملية التطوير ، يكون من الضروري في كثير من الأحيان استخدام طريقة Clone () لنسخ الكائنات. سوف تتيح لك هذه المقالة فهم ماهية Shadow Clone و Deep Clone ، وتفهم خلافاتهم ومزاياها وعيوبها.
هل أنت مرتبك قليلاً عندما ترى هذا العنوان: تنص لغة جافا بوضوح على أن المؤشرات قد تم إلغاؤها ، لأن المؤشرات غالبًا ما تكون مريحة وأيضًا السبب الجذري لانعدام الأمن ، كما أنها تجعل البرنامج معقدًا للغاية ويصعب فهمه. الرمز الذي كتبه إساءة استخدام المؤشرات لا يقل عن استخدام بيان "GOTO" سيئ السمعة بالفعل. من الحكمة للغاية على جافا التخلي عن مفهوم المؤشرات. ولكن هذا فقط أنه لا يوجد تعريف مؤشر واضح في لغة جافا. في جوهرها ، كل بيان جديد يعيد مرجع المؤشر. ومع ذلك ، في معظم الحالات ، لا داعي للقلق بشأن كيفية تشغيل هذا "المؤشر" ، ناهيك عن أن تكون خائفًا مثل تشغيل مؤشرات في C ++. الشيء الوحيد الذي يهتم أكثر هو عند تمرير الكائنات إلى الوظائف.
حزمة com.zoer.src ؛ الطبقة العامة objref {obj aobj = new obj () ؛ int int = 11 ؛ public void changeObj (obj inobj) {inobj.str = "value value" ؛ } public void changePri (int inint) {inint = 22 ؛ } public static void main (string [] args) {objref oref = new objref () ؛ system.out.println ("قبل call changeObj () طريقة:" + oref.aobj) ؛ oref.ChangeObj (oref.aobj) ؛ system.out.println ("بعد call changeObj () طريقة:" + oref.aobj) ؛ System.out.println ("============================================================================================================================ =============================================================================================================================================== =============================================================================================================================================== =============================================================================================================================================== Call ChangePri () الطريقة: " + oref.aint) ؛ }} حزمة com.zoer.src ؛ الفئة العامة obj {string str = "init value" ؛ السلسلة العامة tostring () {return str ؛ }} يدعو الجزء الرئيسي من هذا الرمز طريقتين متشابهتين للغاية ، changeOBJ () و changepri (). الفرق الوحيد هو أن المرء يأخذ الكائن كمعلمة إدخال ، والآخر يأخذ النوع الأساسي int في Java كمعلمة الإدخال. ويتم تغيير معلمات الإدخال داخل كلا جثتي الوظائف. نفس الطريقة على ما يبدو ، ولكن نتائج إخراج البرنامج مختلفة. تغير طريقة changeOBJ () حقًا معلمات الإدخال ، في حين أن طريقة changePri () لا تغير معلمات الإدخال.
من هذا المثال ، تقوم Java بمعالجة الكائنات وأنواع البيانات الأساسية بشكل مختلف. مثل C ، عندما يتم تمرير أنواع البيانات الأساسية لـ Java (مثل int ، char ، double ، إلخ) إلى جسم الوظيفة كمعلمات دخول ، تصبح المعلمات التي تم تمريرها متغيرات محلية داخل جسم الوظيفة. هذا المتغير المحلي هو نسخة من معلمات الإدخال. جميع العمليات داخل هيئة الوظيفة هي عمليات لهذه النسخة. بعد اكتمال تنفيذ الوظيفة ، سيقوم المتغير المحلي بإكمال مهمته ولن يؤثر على المتغيرات كمعلمات إدخال. وتسمى طريقة تمرير المعلمة هذه "تمرير القيمة". في Java ، تم تمرير الممر باستخدام كائن كمعلمة إدخال هو "مرجع مرجعي" ، مما يعني أنه يتم تمرير "مرجع" واحد فقط للكائن. مفهوم هذا "المرجع" هو نفس مرجع المؤشر بلغة C. عندما يتم تغيير متغير الإدخال داخل جسم الوظيفة ، يكون ذلك في الأساس عملية مباشرة على الكائن.
باستثناء "مرجع مرجعي" عند تمرير قيمة الوظيفة ، "مرجع مرجعي" عند تعيين القيم لمتغيرات الكائن باستخدام "=". إنه مشابه لإعطاء متغير اسم مستعار آخر. يشير كلا الاسمين إلى نفس الكائن في الذاكرة.
في البرمجة الفعلية ، نواجه هذا الموقف غالبًا: هناك كائن A يحتوي على بعض القيم الصحيحة في لحظة معينة. في هذا الوقت ، قد تكون هناك حاجة إلى كائن جديد B تمامًا كما هو الحال ، ولن تؤثر أي تغييرات على B على القيمة في A. أي أن A و B هما كائنان مستقلان ، ولكن يتم تحديد القيمة الأولية لـ B بواسطة الكائن A. في لغة Java ، لا يمكن أن يؤدي استخدام بيانات التعيين البسيطة إلى تلبية هذا المطلب. على الرغم من وجود العديد من الطرق لتلبية هذه الحاجة ، فإن أبسط وأكثر الطرق فعالية لتنفيذ طريقة Clone () هي من بينها.
ترث جميع فئات Java فئة java.lang.object بشكل افتراضي ، وهناك طريقة استنساخ () في فئة java.lang.object. تشرح وثائق JDK API أن هذه الطريقة ستعيد نسخة من كائن الكائن. هناك نقطتان يتم شرحهما: أولاً ، يقوم كائن النسخ بإرجاع كائن جديد ، وليس مرجعًا. ثانياً ، فإن الفرق بين نسخ كائن وكائن جديد يتم إرجاعه مع المشغل الجديد هو أن هذه النسخة تحتوي بالفعل على بعض المعلومات حول الكائن الأصلي ، بدلاً من المعلومات الأولية للكائن.
كيفية تطبيق طريقة clone ()؟
دعوة نموذجية للغاية لرمز استنساخ () هي كما يلي:
ينفذ cloneclass من الطبقة العامة clonable {public int int ؛ كائن عام clone () {cloneclass o = null ؛ حاول {o = (cloneclass) super.clone () ؛ } catch (clonenotsupportedException e) {E.PrintStackTrace () ؛ } إرجاع o ؛ }} هناك ثلاث نقاط تستحق الإشارة. أولاً ، تنفذ فئة cloneclass التي تأمل في تنفيذ وظيفة استنساخ الواجهة المستنسخة. هذه الواجهة تنتمي إلى حزمة java.lang. تم استيراد حزمة java.lang إلى الفئة الافتراضية ، لذلك لا يلزم كتابتها باسم java.lang.clonable. شيء آخر يستحق الإشارة إليه هو أن طريقة clone () مثقلة. أخيرًا ، يتم استدعاء super.clone () في طريقة clone () ، مما يعني أيضًا أنه بغض النظر عن شكل بنية الميراث لفئة الاستنساخ ، Super.clone () يستدعي بشكل مباشر أو غير مباشر طريقة clone () لفئة java.lang.object. دعنا نوضح هذه النقاط بالتفصيل أدناه.
يجب أن يقال أن النقطة الثالثة هي الأكثر أهمية. إذا لاحظت بعناية طريقة أصلية لاستنساخ فئة الكائن () ، فإن كفاءة الطريقة الأصلية أعلى بكثير من تلك الموجودة في الأساليب غير الأصلية في Java. هذا ما يفسر أيضًا سبب استخدام طريقة clone () في الكائن بدلاً من فئة جديدة الأولى ثم تعيين المعلومات من الكائن الأصلي إلى الكائن الجديد ، على الرغم من أن هذا يمنع وظيفة الاستنساخ أيضًا. بالنسبة للنقطة الثانية ، يجب أن نلاحظ أيضًا ما إذا كان clone () في فئة الكائن طريقة ذات خاصية محمية. هذا يعني أيضًا أنه إذا كنت ترغب في تطبيق طريقة Clone () ، فيجب أن ترث فئة الكائن. في جافا ، ترث جميع الفئات فئة الكائن بشكل افتراضي ، لذلك لا داعي للقلق بشأن هذا. ثم الزائد في طريقة clone (). شيء آخر يجب مراعاته هو أنه من أجل أن تستدعي الفئات الأخرى طريقة Clone () من هذه الفئة المستنسخة ، بعد التحميل الزائد ، يجب ضبط سمات طريقة clone () على الأماكن العامة.
فلماذا لا تزال فئة الاستنساخ تنفذ الواجهة المستنسخة؟ لاحظ أن الواجهة المستنسخة لا تحتوي على أي طرق! في الواقع ، هذه الواجهة هي مجرد علامة ، وهذه العلامة مخصصة فقط لطريقة clone () في فئة الكائن. إذا لم تنفذ فئة الاستنساخ الواجهة المستنسخة وتدعو طريقة clone () للكائن (أي ، تسمى طريقة super.clone ()) ، فإن طريقة استثمار الكائن () سوف ترمي استثناء clonenotsupportedException.
ما سبق هي الخطوات الأساسية لاستنساخ. إذا كنت ترغب في إكمال استنساخ ناجح ، فأنت بحاجة أيضًا إلى فهم ماهية "Shadow Clone" و "Deep Clone".
ما هو Shadow Clone؟
حزمة com.zoer.src ؛ فئة revelonea {private int i ؛ reciletea العام (int II) {i = ii ؛ } public void doublevalue () {i *= 2 ؛ } السلسلة العامة tostring () {return integer.toString (i) ؛ }} Class Cloneb تنفذ clonable {public int int ؛ inclonea unca العام = new displonea (111) ؛ كائن عام clone () {cloneb o = null ؛ حاول {o = (cloneb) super.clone () ؛ } catch (clonenotsupportedException e) {E.PrintStackTrace () ؛ } إرجاع o ؛ }} الفئة العامة objref {public static void main (string [] a) {cloneb b1 = new cloneb () ؛ b1.aint = 11 ؛ System.out.println ("قبل clone ، b1.aint =" + b1.aint) ؛ System.out.println ("قبل clone ، b1.UNCA =" + b1.UNCA) ؛ cloneb b2 = (cloneb) b1.clone () ؛ b2.aint = 22 ؛ b2.UNCA.DOUBLEVALUE () ؛ System.out.println ("======================================================================================================================================== ============================================================= =============================================================== ============================================================= =============================================================== ============================================================= =============================================================== clone ، b2.aint = " + b2.aint) ؛ System.out.println ("بعد clone ، b2.unca =" + b2.unca) ؛ }} نتيجة الإخراج:
قبل استنساخ ، b1.aint = 11before clone ، b1.unca = 111 ======================================================== =========================================================== ========================================================== =========================================================== ========================================================== =========================================================== ========================================================== ===========================================================
تُظهر نتائج الإخراج أن المتغير int ونتائج استنساخ كائن مثيل UNCA من الاستنشاق غير متسقة. نوع int هو استنساخ حقًا لأن متغير AINT في B2 ليس له أي تأثير على AINT of B1. وهذا يعني ، B2.aint و B1.Aink يشغلان بالفعل مساحات ذاكرة مختلفة ، b2.aint هي نسخة حقيقية من b1.aint. على العكس من ذلك ، يتغير التغيير إلى B2.UNCA في وقت واحد إلى B1.UNCA ، ومن الواضح أن B2.UNCA و B1.UNCA هي مراجع مختلفة تشير فقط إلى نفس الكائن! من هذا ، يمكننا أن نرى أن تأثير استدعاء طريقة استنساخ () في فئة الكائن هو: أولاً افتح مساحة في الذاكرة التي هي نفس الكائن الأصلي ، ثم نسخ المحتويات في الكائن الأصلي كما هي. بالنسبة لأنواع البيانات الأساسية ، فإن مثل هذه العمليات ليست مشكلة ، ولكن بالنسبة لمتغيرات النوع غير المحدد ، فإننا نعلم أنها توفر فقط الإشارات إلى الكائنات ، والتي تتسبب أيضًا في متغيرات النوع غير المحدد بعد استنساخ للإشارة إلى نفس الكائن والمتغيرات المقابلة في الكائن الأصلي.
في معظم الأوقات ، غالبًا ما لا تكون نتائج هذا الاستنساخ هي النتائج التي نأملها ، ويسمى هذا الاستنساخ أيضًا "استنساخ الظل". لجعل B2.UNCA يشير إلى كائن مختلف عن B2.UNCA ، ويحتوي B2.UNCA أيضًا على معلومات في B1.UNCA كمعلومات أولية ، يجب عليك تنفيذ استنساخ العمق.
كيف تؤدي استنساخ عميق؟
من السهل جدًا تغيير المثال أعلاه إلى استنساخ عميق ، ويحتاج تغييران: أحدهما هو السماح لفئة الاستنكار أيضًا بتنفيذ وظيفة استنساخ نفسها مثل فئة الاستنساخ (تنفيذ الواجهة المستنسخة وتحميل طريقة Clone ()). والثاني هو إضافة جملة O.UNCA = (INCLONTEA) unca.clone () إلى طريقة clone () من cloneb ؛
حزمة com.zoer.src ؛ تنفذ الفئة reclonea clonable {private int i ؛ reciletea العام (int II) {i = ii ؛ } public void doublevalue () {i *= 2 ؛ } السلسلة العامة tostring () {return integer.toString (i) ؛ } كائن عام clone () {inclonea o = null ؛ جرب {o = (rectonea) super.clone () ؛ } catch (clonenotsupportedException e) {E.PrintStackTrace () ؛ } إرجاع o ؛ }} Class Cloneb تنفذ clonable {public int int ؛ inclonea unca العام = new displonea (111) ؛ كائن عام clone () {cloneb o = null ؛ حاول {o = (cloneb) super.clone () ؛ } catch (clonenotsupportedException e) {E.PrintStackTrace () ؛ } O.UNCA = (rectonea) unca.clone () ؛ العودة س ؛ }} الفئة العامة clonemain {public static void main (string [] a) {cloneb b1 = new cloneb () ؛ b1.aint = 11 ؛ System.out.println ("قبل clone ، b1.aint =" + b1.aint) ؛ System.out.println ("قبل clone ، b1.UNCA =" + b1.UNCA) ؛ cloneb b2 = (cloneb) b1.clone () ؛ b2.aint = 22 ؛ b2.UNCA.DOUBLEVALUE () ؛ System.out.println ("============================================================================== ============================================================================================================================ ============================================================================================================================ ========================================================================================================================== clone ، b1.aint = " + b1.aint) ؛ System.out.println ("بعد clone ، b1.UNCA =" + B1.UNCA) ؛ System.out.println("=================================================================================================== ========================================================== ========================================================== ========================================================== ========================================================== ========================================================== ========================================================== نتيجة الإخراج:
قبل استنساخ ، b1.aint = 11before clone ، b1.unca = 111 ======================================================== =========================================================== ========================================================== =========================================================== ========================================================== =========================================================== ========================================================== ===========================================================
يمكن ملاحظة أن التغيير الحالي لـ B2.UNCA ليس له أي تأثير على B1.UNCA. في هذا الوقت ، يشير B1.UNCA و B2.UNCA إلى حالتين مختلفتين من الاستنشاق ، و B1 و B2 لهما نفس القيمة في اللحظة التي يتم فيها استنساخ B2 = (cloneb) b1.clone () ؛ يسمى ، هنا ، b1.i = b2.i = 11.
يجب أن تعلم أنه لا يمكن لجميع الفصول تنفيذ الحيوانات المستنسخة العميقة. على سبيل المثال ، إذا قمت بتغيير متغير Type Type في فئة CloneB أعلاه إلى نوع StringBuffer ، فابحث عن الوصف حول StringBuffer في API JDK. لا يفرط StringBuffer على طريقة Clone () ، وما هو أكثر جدية هو أن StringBuffer لا يزال فئة نهائية ، مما يعني أنه لا يمكننا تنفيذ استنساخ StringBuffer بشكل غير مباشر باستخدام الميراث. إذا كانت الفئة تحتوي على كائن نوع stringBuffer أو كائن مشابه لـ StringBuffer ، فلدينا خياران: إما أن يتم تنفيذ استنساخ الظل فقط ، أو إضافة جملة إلى طريقة clone () للفئة (على افتراض أنه كائن sringbuffer ، والاسم المتغير لا يزال UNCA): O.UNCA = جديد (unca.tosring ()) ؛ // الأصلي هو: O.UNCA = (INCELONEA) unca.clone () ؛
تجدر الإشارة أيضًا إلى أنه بالإضافة إلى أنواع البيانات الأساسية التي يمكنها تنفيذ الحيوانات المستنسخة العميقة تلقائيًا ، فإن كائن السلسلة هو استثناء. يبدو أن أدائها بعد استنساخ ينفذ أيضًا استنساخًا عميقًا. على الرغم من أن هذا مجرد وهم ، إلا أنه يسهل برامجنا بشكل كبير.
يجب شرح الفرق بين السلسلة و stringbuffer في استنساخ أن هذا ليس تركيزًا على الفرق بين السلسلة و stringbuffer ، ولكن من هذا المثال ، يمكننا أيضًا رؤية بعض الميزات الفريدة لفئة السلسلة.
المثال التالي يتضمن فئتين. تحتوي فئة Clonec على متغير نوع السلسلة ومتغير نوع StringBuffer ، وتنفذ طريقة Clone (). يتم الإعلان عن متغير نوع Clonec C1 في فئة Strclone ، ثم يتم استدعاء طريقة Clone () لـ C1 لإنشاء نسخة من C1 C2. بعد تغيير متغيرات نوع السلسلة و stringbuffer في C2 ، تتم طباعة النتيجة:
حزمة com.zoer.src ؛ Class Clonec ينفذ clonable {public string str ؛ StringBuffer STRBUFF العام ؛ كائن عام clone () {clonec o = null ؛ حاول {o = (clonec) super.clone () ؛ } catch (clonenotsupportedException e) {E.PrintStackTrace () ؛ } إرجاع o ؛ }} الفئة العامة strclone {public static void main (string [] a) {clonec c1 = new clonec () ؛ C1.Str = سلسلة جديدة ("initializester") ؛ c1.strbuff = new StringBuffer ("initializestrbuff") ؛ System.out.println ("قبل clone ، c1.str =" + c1.Str) ؛ System.out.println ("قبل clone ، 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 = "font-size: 16px ؛"> قبل clone ، c1.str = initializestr قبل استنساخ ، c1.strbuff = initializestrbuff ===================================================================================================================================================================سف ===================================================================================================================================================================سف ===================================================================================================================================================================سف ===================================================================================================================================================================سف 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. وذلك لأن السلسلة مكتوبة كفئة غير قابلة للتغيير من قبل مهندسي Sun ، ولا يمكن للوظائف في جميع فئات السلسلة تغيير قيمهم الخاصة.
ما سبق هو كل شيء عن هذا المقال. آمل أن يكون من المفيد أن يفهم الجميع النسخ المتماثل العميق والضحل في جافا.