الغرض من هذه المقالة هو تقديم جافا جيرلايس ، بحيث يمكن للجميع الحصول على فهم نهائي وواضح ودقيق لجميع جوانب جافا جيرلسي ، وكذلك وضع الأساس للمقال التالي "إعادة فهم Java Reflection".
مقدمة
الأدوية هي نقطة معرفة مهمة للغاية في جافا. تستخدم الأدوية الجيرية على نطاق واسع في أطر عمل Java. في هذه المقالة ، سوف ننظر في تصميم جافا جيرلايس من الصفر ، والتي ستشمل معالجة البطاقات البرية ومحو نوع المؤسسة.
أساسيات عامة
فصول عامة
دعنا أولاً نحدد فئة مربع بسيطة:
مربع الفئة العامة {كائن سلسلة خاصة ؛ مجموعة void العامة (كائن سلسلة) {this.object = object ؛ } السلسلة العامة get () {return Object ؛ }}هذه هي الممارسة الأكثر شيوعا. أحد عيوب هذا هو أنه يمكن تحميل عناصر من النوع الوطني فقط في المربع. في المستقبل ، إذا كنا بحاجة إلى تحميل أنواع أخرى من العناصر مثل عدد صحيح ، فيجب علينا أيضًا إعادة كتابة مربع آخر. لا يمكن إعادة استخدام الكود ، ويمكن استخدام الأدوية الجينية حل هذه المشكلة بشكل جيد.
مربع الفئة العامة <T> {// t يرمز إلى "type" private t t ؛ مجموعة void العامة (t t) {this.t = t ؛ } t get () {return t ؛ }}وبهذه الطريقة ، يمكن إعادة استخدام فئة الصندوق لدينا ، ويمكننا استبدال T بأي نوع نريده:
Box <integer> IntegerBox = new box <integer> () ؛ المربع <double> doublebox = new box <bourent> () ؛ box <String> stringbox = new box <String> () ؛
طرق عامة
بعد قراءة الفصل العام ، دعونا نتعرف على الأساليب العامة. إن إعلان طريقة عامة بسيطة ، فقط أضف نموذجًا مشابهًا لـ <k ، v> إلى نوع الإرجاع:
الفئة العامة util {public static <k ، v> boolean compare (pair <k ، v> p1 ، pair <k ، v> p2) {return p1.getkey (). equals (p2.getKey ()) }} زوج الفئة العامة <K ، V> {private K Key ؛ قيمة V الخاصة ؛ Public Pair (K Key ، v value) {this.key = key ؛ this.value = القيمة ؛ } public void setKey (k key) {this.key = key ؛ } public void setValue (v value) {this.value = value ؛ } public K getKey () {return Key ؛ } public v getValue () {return value ؛ }}يمكننا استدعاء طرق عامة مثل هذا:
الزوج <integer ، string> p1 = new pair <> (1 ، "Apple") ؛ الزوج <integer ، string> p2 = new pair <> (2 ، "pear") ؛ Boolean Same = Util. <Integer ، String> مقارنة (p1 ، p2) ؛
أو استخدم نوع الاستدلال في النوع في Java 1.7/1.8 للسماح Java تلقائيًا باستنتاج معلمات النوع المقابل:
الزوج <integer ، string> p1 = new pair <> (1 ، "Apple") ؛ الزوج <integer ، string> p2 = new pair <> (2 ، "pear") ؛ Boolean same = util.compare (p1 ، p2) ؛
رمز الحدود
الآن نريد تنفيذ مثل هذه الوظيفة للعثور على عدد العناصر في صفيف عام أكبر من عنصر معين. يمكننا تنفيذها مثل هذا:
ثابت عام <T> int countgreaterthan (t [] anarray ، t elem) {int count = 0 ؛ لـ (t e: anarray) if (e> elem) // error error ++ count ؛ عدد العائد ؛}ولكن من الواضح أن هذا خطأ ، لأنه باستثناء الأنواع البدائية مثل int و int أو مزدوج أو طويل أو تعويم أو بايت أو شار ، وما إلى ذلك ، قد لا تستخدم الفئات الأخرى بالضرورة للمشغلين> ، وبالتالي فإن المترجم يبلغ عن خطأ. كيف تحل هذه المشكلة؟ الجواب هو استخدام رمز الحدود.
الواجهة العامة قابلة للمقارنة <t> {public int compareto (t o) ؛}قم بإعداد إعلان مشابه لما يلي ، وهو ما يعادل إخبار المترجم بأن المعلمة type تمثل الفئات التي تنفذ الواجهة المماثلة ، والتي تعادل إخبار المحول البرمجي بأنها جميعها على الأقل طريقة المقارنة.
الثابت العام <t يمتد قابلة للمقارنة <t >> int countgreaterthan (t [] anarray ، t elem) {int count = 0 ؛ لـ (t e: anarray) if (e.compareto (eLEM)> 0) ++ count ؛ عدد العائد ؛}البرية
قبل فهم البطاقات البرية ، يجب علينا أولاً توضيح مفهوم ، أو استعارة فئة الصندوق التي حددناها أعلاه ، لنفترض أننا نضيف طريقة كهذه:
public void boxtest (Box <Number> n) { / * ... * /}إذن ما نوع المعلمات التي يسمح بها المربع <number> n للقبول؟ هل يمكننا المرور في المربع <integer> أو المربع <double>؟ الجواب لا. على الرغم من أن عدد صحيح ومزدوج من الفئات الفرعية للرقم ، لا توجد علاقة بين المربع <integer> أو المربع <double> و Box <Number> في الأدوية الجماهيرية. هذا مهم للغاية ، وسنستخدم مثالًا كاملاً لتعميق فهمنا.
أولاً ، نحدد بعض الفصول البسيطة ، وسنستخدمها أدناه:
فئة الفاكهة {} class Apple تمتد الفاكهة {} الفئة البرتقالية يمتد الفاكهة {}في المثال التالي ، نقوم بإنشاء قارئ فئة عامة ، ثم في F1 () ، عندما نحاول Fruit F = fruitReader.ReadExact (التفاح) ؛ سيقوم برنامج التحويل البرمجي بالإبلاغ عن خطأ لأنه لا توجد علاقة بين القائمة <urom> والقائمة <Apple>.
الطبقة العامة genericreading {static list <Apple> Apples = Arrays.Aslist (New Apple ()) ؛ القائمة الثابتة <urom> fruit = arrays.aslist (new fruit ()) ؛ قارئ الفئة الثابت <T> {t readExact (قائمة <T> قائمة) {return list.get (0) ؛ }} static void f1 () {reader <Urut> fruitReader = New Reader <Tuph> () ؛ // الأخطاء: لا يمكن تطبيق قائمة <Tuph> على القائمة <Apple>. // fruit f = fruitReader.ReadExact (التفاح) ؛ } public static void main (string [] args) {f1 () ؛ }}ولكن وفقًا لعادات التفكير المعتادة لدينا ، يجب أن يكون هناك صلة بين التفاح والفواكه ، لكن المترجم لا يمكنه التعرف عليه. فكيف يمكنني حل هذه المشكلة في الكود العام؟ يمكننا حل هذه المشكلة باستخدام البطاقات البرية:
فئة ثابتة CovarianTreader <T> {t readcovariant (قائمة <؟ تمديد t> list) {return list.get (0) ؛ }} static void f2 () {covariantreader <ruit> fruitReader = جديد covariantreader <ruit> () ؛ الفاكهة f = fruitReader.ReadCovariant (الفاكهة) ؛ الفاكهة A = fruitReader.ReadCovariant (التفاح) ؛} الفراغ الثابت العام الرئيسي (سلسلة [] args) {f2 () ؛}هذا مشابه تمامًا لإخبار المترجم أن المعلمات المقبولة بواسطة طريقة إعادة المتغيرات من FruitReader هي طالما أن الفئة الفرعية التي ترضي الفاكهة (بما في ذلك الفاكهة نفسها) ، بحيث ترتبط العلاقة بين الفئة الفرعية والفئة الأم أيضًا.
مبادئ PECS
رأينا استخدامًا مشابهًا لـ <؟ يمتد t> أعلاه. باستخدامه ، يمكننا الحصول على عناصر من القائمة ، هل يمكننا إضافة عناصر إلى القائمة؟ لنجربها:
الطبقة العامة menericsandcovariance {public static void main (string [] args) {// wildcards تسمح بالتغاير: قائمة <؟ يمتد الفاكهة> flist = new ArrayList <Apple> () ؛ // خطأ ترجمة: لا يمكن إضافة أي نوع من الكائنات: // flist.add (Apple () جديدة) // flist.add (new Orange ()) // flist.add (new fruit ()) // flist.add (new Object () flist.add (null) ؛ // قانوني ولكن غير مهتم// نعلم أنه يعود على الأقل الفاكهة: الفاكهة f = flist.get (0) ؛ }}الجواب هو لا ، لم يسمح لنا برنامج التحويل البرمجي Java بالقيام بذلك ، لماذا؟ قد ننظر أيضًا في هذه المشكلة من منظور المترجم. لأن القائمة <؟ يمتد الفاكهة> يمكن أن يكون للمشرفة العديد من المعاني:
قائمة <؟ يمتد Fruit> flist = new ArrayList <Urtive> () ؛ قائمة <؟ يمتد الفاكهة> flist = new ArrayList <Apple> () ؛ قائمة <؟ يمتد الفاكهة> flist = new ArrayList <Orange> () ؛
لذلك ، لفصول التجميع التي تنفذ <؟ يمتد t> ، لا يمكن اعتبارها سوى منتج يقدم (الحصول) عنصرًا (الحصول على) إلى الخارج ، ولا يمكن استخدامه كمستهلك للحصول على (إضافة) عناصر إلى الخارج.
ماذا يجب أن نفعل إذا أردنا إضافة العنصر؟ يمكنك استخدام <؟ سوبر تي>:
الطبقة العامة العامة {قائمة ثابتة <Apple> Apples = ArrayList New ArgleList <Apple> () ؛ القائمة الثابتة <urom> fruit = new ArrayList <Uruile> () ؛ ثابت <t> void writeExact (قائمة <T> قائمة ، t العنصر) {list.add (item) ؛ } static void f1 () {writeExact (Apples ، New Apple ()) ؛ WriteExact (الفاكهة ، التفاح الجديد ()) ؛ } static <T> void writewithwildcard (list <؟ super t> list ، t item) {list.add (item)} static void f2 () {writeWithwildCard (Apples ، New Apple ()) ؛ WriteWithWildCard (Fruit ، New Apple ()) ؛ } public static void main (string [] args) {f1 () ؛ F2 () ؛ }}وبهذه الطريقة ، يمكننا إضافة عناصر إلى الحاوية ، ولكن عيب استخدام Super هو أنه لا يمكننا الحصول على عناصر في الحاوية في المستقبل. السبب بسيط للغاية. نستمر في النظر في هذه القضية من منظور المترجم. للقائمة <؟ قائمة Super Apple> ، يمكن أن يكون لها المعاني التالية:
قائمة <؟ Super Apple> list = new ArrayList <Apple> () ؛ قائمة <؟ Super Apple> list = new ArrayList <Tuph> () ؛ قائمة <؟ Super Apple> list = new ArrayList <Object> () ؛
عندما نحاول الحصول على تفاحة من خلال قائمة ، قد نحصل على فاكهة ، والتي يمكن أن تكون أنواعًا أخرى من الفاكهة مثل البرتقال.
بناءً على المثال أعلاه ، يمكننا تلخيص قاعدة ، "تمتد المنتج ، المستهلك Super":
بعد قراءة بعض رمز مصدر مجموعات Java ، يمكننا أن نجد أننا عادة ما نستخدم الاثنين معًا ، مثل ما يلي:
مجموعات الفئة العامة {public static <T> نسخة void (قائمة <؟ super t> dest ، قائمة <؟ تمديد t> src) {for (int i = 0 ؛ i <src.size () ؛ i ++) dest.set (i ، src.get (i)) ؛ }}اكتب مسح
ربما يكون الشيء الأكثر إثارة للقلق في جافا جيرلكس هو محو النوع ، خاصة بالنسبة للمبرمجين الذين لديهم تجربة C ++. تعني محو النوع أنه لا يمكن استخدام Java Generics إلا لفحص النوع الثابت أثناء التجميع ، ثم الكود الذي تم إنشاؤه بواسطة المترجم سوف يمحو معلومات النوع المقابل. وبهذه الطريقة ، أثناء التشغيل ، يعرف JVM في الواقع النوع المحدد الذي يمثله عام. والغرض من ذلك هو أن جافا الأدوية الجيردية تم تقديمها بعد 1.5. من أجل الحفاظ على التوافق الهبوطي ، يمكنك فقط القيام بمحو النوع لتكون متوافقة مع الكود السابق غير العام. لهذه النقطة ، إذا قرأت الكود المصدري لإطار مجموعة Java ، فيمكنك أن تجد أن بعض الفصول لا تدعم بالفعل الأداء العام.
بعد قوله كثيرًا ، ماذا تعني المحو العام؟ دعونا نلقي نظرة أولاً على المثال البسيط التالي:
عقدة الفئة العامة <T> {Private T Data ؛ العقدة الخاصة <T> التالية ؛ العقدة العامة (بيانات t ، العقدة <T> التالية)} this.data = data ؛ this.next = التالي ؛ } public t getData () {return data ؛ } // ...}بعد أن يكمل المترجم فحص النوع المقابل ، سيتم بالفعل تحويل الرمز أعلاه إلى:
عقدة الفئة العامة {بيانات الكائنات الخاصة ؛ العقدة الخاصة المقبل ؛ العقدة العامة (بيانات الكائن ، العقدة التالية) {this.data = data ؛ this.next = التالي ؛ } الكائن العام getData () {return data ؛ } // ...}هذا يعني أنه بغض النظر عما إذا كنا نعلن العقدة <string> أو العقدة <integer> ، فإن JVM تعتبر جميعها عقدة <object> أثناء وقت التشغيل. هل هناك أي طريقة لحل هذه المشكلة؟ هذا يتطلب منا إعادة ضبط الحدود بأنفسنا وتعديل الكود أعلاه إلى ما يلي:
عقدة الفئة العامة <T تمتد قابلة للمقارنة <T >> {Private T Data ؛ العقدة الخاصة <T> التالية ؛ العقدة العامة (بيانات t ، العقدة <T> التالية) {this.data = data ؛ this.next = التالي ؛ } public t getData () {return data ؛ } // ...}وبهذه الطريقة ، سيحل المترجم محل المكان الذي يظهر فيه T مع قابلية للمقارنة بدلاً من الكائن الافتراضي:
عقدة الفئة العامة {بيانات قابلة للمقارنة الخاصة ؛ العقدة الخاصة المقبل ؛ العقدة العامة (بيانات قابلة للمقارنة ، العقدة التالية) {this.data = data ؛ this.next = التالي ؛ } getData getData () {return data ؛ } // ...}قد يكون المفهوم أعلاه أسهل في الفهم ، ولكن في الواقع ، فإن المحو العام يجلب المزيد من المشكلات. بعد ذلك ، دعونا نلقي نظرة منهجية على بعض المشكلات التي تسببها محو النوع. قد لا تتم مواجهة بعض المشكلات في C ++ Generics ، ولكن يجب أن تكون حذراً للغاية في Java.
السؤال 1
المصفوفات العامة غير مسموح بها في جافا. إذا قام المترجم بشيء مثل ما يلي ، فسيقوم بالإبلاغ عن خطأ:
قائمة <integer> [] arrayoflists = قائمة جديدة <integer> [2] ؛ // خطأ في وقت الترجمة
لماذا لا يدعم المترجم الممارسة أعلاه؟ استمر في استخدام التفكير العكسي ، ونحن نعتبر هذه المشكلة من منظور المترجم.
دعونا نلقي نظرة أولاً على المثال التالي:
كائن [] سلاسل = سلسلة جديدة [2] ؛ سلاسل [0] = "مرحبا" ؛ // OKSTRINGS [1] = 100 ؛ // يتم إلقاء ArrayStoreException.
الرمز أعلاه سهل الفهم. لا يمكن لصفائف السلسلة تخزين عناصر عدد صحيح ، وغالبًا ما تحتاج هذه الأخطاء إلى اكتشافها حتى يتم تشغيل الرمز ، ولا يمكن للمترجم التعرف عليها. بعد ذلك ، دعونا نلقي نظرة على ما سيحدث إذا دعمت Java إنشاء صفائف عامة:
Object [] StringLists = New List <String> [] ؛ // خطأ في برنامج التحويل البرمجي ، ولكن يتظاهر أنه مسموح به stringlists [0] = جديد ArrayList <string> () ؛ // OK // يجب إلقاء ArrayStoreException ، لكن وقت التشغيل لا يمكنه اكتشافه.
لنفترض أننا ندعم إنشاء صفائف عامة. نظرًا لأن معلومات النوع أثناء وقت التشغيل قد تم محوها ، فإن JVM في الواقع لا يعرف الفرق بين ArrayList الجديد <string> () و ArrayList الجديد <integer> () على الإطلاق. إذا حدثت مثل هذه الأخطاء في سيناريوهات التطبيق العملي ، فسيكون من الصعب للغاية اكتشافها.
إذا كنت لا تزال متشككًا في ذلك ، فيمكنك محاولة تشغيل الكود التالي:
الفئة العامة eracedtypeequivalence {public static void main (string [] args) {class c1 = new ArrayList <string> (). getClass () ؛ الفئة C2 = new ArrayList <integer> (). getClass () ؛ System.out.println (C1 == C2) ؛ // حقيقي }}السؤال 2
استمر في إعادة استخدام فئة العقدة لدينا أعلاه. بالنسبة للرمز العام ، سيساعدنا برنامج التحويل البرمجي Java سراً في تنفيذ طريقة الجسر.
عقدة الفئة العامة <T> {Public T Data ؛ العقدة العامة (t data) {this.data = data ؛ } public void setData (t data) {system.out.println ("node.setData") ؛ this.data = البيانات ؛ }} الفئة العامة myNode تمتد العقدة <integer> {public myNode (بيانات integer) {super (data) ؛ } public void setData (Integer Data) {system.out.println ("mynode.setData") ؛ super.setData (البيانات) ؛ }}بعد قراءة التحليل أعلاه ، قد تظن أنه بعد محو النوع ، سيحول المترجم العقدة و mynode إلى ما يلي:
عقدة الفئة العامة {بيانات الكائنات العامة ؛ العقدة العامة (بيانات الكائن) {this.data = data ؛ } public void setData (كائن بيانات) {system.out.println ("node.setData") ؛ this.data = البيانات ؛ }} الفئة العامة mynode تمتد العقدة {public mynode (بيانات Integer) {super (data) ؛ } public void setData (Integer Data) {system.out.println ("mynode.setData") ؛ super.setData (البيانات) ؛ }}في الواقع ، هذا ليس هو الحال. دعونا نلقي نظرة أولاً على الكود التالي. عند تشغيل هذا الرمز ، سيتم طرح ClassCastException ، مما يدفع إلى أنه لا يمكن تحويل السلسلة إلى عدد صحيح:
mynode mn = new mynode (5) ؛ node n = mn ؛ // نوع RAW - يرمي برنامج التحويل البرمجي تحذيرًا لم يتم التحقق منه ("Hello") ؛ // يسبب إلقاء classcastexception .// Integer X = Mn.Data ؛إذا اتبعنا الرمز الذي أنشأناه أعلاه ، يجب ألا نقوم بالإبلاغ عن خطأ عند التشغيل إلى السطر 3 (لاحظ أنني علقت السطر 4) ، لأن طريقة SetData (بيانات السلسلة) غير موجودة في MyNode ، لذلك يمكننا فقط استدعاء طريقة SetData (بيانات الكائن) لعقدة الفئة الأصل. منذ هذه الطريقة ، لا ينبغي أن يبلغ رمز السطر 3 أعلاه عن خطأ ، لأنه يمكن بالطبع تحويل سلسلة الأسلوب إلى كائن ، فكيف يتم إلقاء ClassCastException؟
في الواقع ، يتولى برنامج التحويل البرمجي Java تلقائيًا الكود أعلاه:
Class MyNode يمتد العقدة {// bridge التي تم إنشاؤها بواسطة برنامج التحويل البرمجي public void setData (بيانات الكائن) {setData ((integer) data) ؛ } public void setData (Integer Data) {system.out.println ("mynode.setData") ؛ super.setData (البيانات) ؛ } // ...}هذا هو السبب في الإبلاغ عن الخطأ أعلاه. عند بيانات setData ((عدد صحيح)) ؛ لا يمكن تحويل السلسلة إلى عدد صحيح. لذلك ، عندما يطالبك المترجم تحذيرًا دون رادع في السطر 2 أعلاه ، لا يمكننا اختيار تجاهله ، وإلا سيتعين علينا الانتظار حتى وقت التشغيل للعثور على الاستثناء. سيكون من الرائع أن أضفنا العقدة <integer> n = Mn في البداية ، بحيث يمكن للمترجم مساعدتنا في العثور على أخطاء مقدمًا.
السؤال 3
كما ذكرنا أعلاه ، لا يمكن لـ Java Generics سوى توفير فحص نوع ثابت إلى حد كبير ، ومن ثم سيتم محو معلومات النوع ، لذلك لن يمرر المترجم بالطريقة التالية لاستخدام معلمات النوع لإنشاء مثيلات:
static static <e> void append (list <e> list) {e elem = new e () ؛ // مجموعة خطأ في وقت الترجمة. ADD (ELEM) ؛}ولكن ماذا يجب أن نفعل إذا أردنا إنشاء مثيلات باستخدام معلمات النوع في سيناريوهات معينة؟ يمكن استخدام الانعكاس لحل هذه المشكلة:
static static <e> void Aspend (list <e> list ، class <e> cls) يلقي الاستثناء {e elem = cls.newinstance () ؛ // OK List.add (elem) ؛}يمكننا أن نسميها مثل هذا:
قائمة <Tring> ls = new ArrayList <> () ؛ إلحاق (LS ، String.Class) ؛
في الواقع ، بالنسبة للمشكلة أعلاه ، يمكنك أيضًا استخدام أنماط تصميم المصنع والقالب لحلها. قد يرغب الأصدقاء المهتمين في إلقاء نظرة على شرح إنشاء مثيل للأنواع في الفصل 15 في التفكير في Java. لن نذهب إلى هنا.
السؤال 4
لا يمكننا استخدام الكلمة الرئيسية لـ antyof مباشرة للرمز العام ، لأن برنامج التحويل البرمجي Java سيمحو جميع معلومات النوع العام ذي الصلة عند إنشاء الرمز ، تمامًا كما لا يمكن لـ JVM التي تم التحقق منها أعلاه التعرف على الفرق بين ArrayList <integer> و arraylist <string> أثناء وقت التشغيل:
ثابت عام <E> void rtti (list <e> list) {if (list electureof arraylist <integer>) {// compile time error // ...}} => {ArrayList <teger> ، arraylist <string> ، linkedList <forme> ، ...}على النحو الوارد أعلاه ، يمكننا استخدام البطاقات البرية لإعادة ضبط الحدود لحل هذه المشكلة:
public static void rtti (list <؟> list) {if (list electionof arrayList <؟>) {// ok ؛ يتطلب مثيل type type // ...}}لخص
ما ورد أعلاه هو كل شيء عن إعادة فهم جافا جيفا جيركس في هذه المقالة ، وآمل أن يكون مفيدًا للجميع. يمكن للأصدقاء المهتمين الاستمرار في الرجوع إلى هذا الموقع:
شرح مفصل لأساسيات صفيف جافا
أساسيات برمجة Java: تقليد مشاركة رمز تسجيل الدخول إلى المستخدم
أساسيات برمجة شبكة Java: اتصال أحادي الاتجاه
إذا كانت هناك أي أوجه قصور ، فيرجى ترك رسالة لإشارةها. شكرا لك يا أصدقائك لدعمكم لهذا الموقع!