كيف يتم تطبيق تعدد الأشكال داخل JVM

تم إعداد ترجمة لهذه المقالة خصيصًا للطلاب في دورة Java Developer.





في مقالتي السابقة ، كل شيء عن طريقة التحميل الزائد مقابل تجاوز الطريقة ، نظرنا في القواعد والاختلافات في التحميل الزائد للطريقة وتجاوزها. في هذه المقالة ، سنرى كيف يتم التعامل مع أسلوب التحميل الزائد والإلغاء داخل JVM.

على سبيل المثال ، خذ الصفوف من المقال السابق: الوالد الثديي (الثدييات) والطفل Human (الإنساني).

 public class OverridingInternalExample { private static class Mammal { public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); } } private static class Human extends Mammal { @Override public void speak() { System.out.println("Hello"); } //   speak() public void speak(String language) { if (language.equals("Hindi")) System.out.println("Namaste"); else System.out.println("Hello"); } @Override public String toString() { return "Human Class"; } } //           public static void main(String[] args) { Mammal anyMammal = new Mammal(); anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa // 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Mammal humanMammal = new Human(); humanMammal.speak(); // Output - Hello // 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Human human = new Human(); human.speak(); // Output - Hello // 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V human.speak("Hindi"); // Output - Namaste // 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V } } 

يمكننا أن ننظر إلى مسألة تعدد الأشكال من الجانبين: من "المنطقي" و "المادي". دعنا أولاً نلقي نظرة على الجانب المنطقي للقضية.

وجهة نظر منطقية


من وجهة نظر منطقية ، في مرحلة التجميع ، تعتبر الطريقة التي تم استدعاءها مرتبطة بنوع المرجع. ولكن في وقت التشغيل ، سيتم استدعاء طريقة الكائن المشار إليه.

على سبيل المثال ، في السطر humanMammal.speak(); يعتقد المحول البرمجي أنه سيتم استدعاء Mammal.speak() ، حيث humanMammal إعلان humanMammal كـ Mammal . ولكن في وقت التشغيل ، ستعلم JVM أن humanMammal يحتوي على كائن Human وسوف يستدعي بالفعل الأسلوب Human.speak() .

الأمر بسيط للغاية طالما بقينا على مستوى مفاهيمي. لكن كيف تتعامل JVM مع كل هذا داخليًا؟ كيف يحسب JVM الطريقة التي يجب أن تسمى؟

نحن نعلم أيضًا أن الطرق المثقلة بأكثر من طاقتها لا تُسمى متعددة الأشكال ويتم حلها في وقت التحويل البرمجي. على الرغم من أن طريقة التحميل الزائد في بعض الأحيان تسمى تعدد زمن الترجمة أو الربط المبكر / الثابت .

أساليب التجاوز (تجاوز) حل في وقت التشغيل لأن المحول البرمجي لا يعرف ما إذا كانت هناك أساليب تجاوز في الكائن المعين إلى الارتباط.

وجهة نظر المادية


في هذا القسم ، سنحاول العثور على أدلة "مادية" لجميع البيانات المذكورة أعلاه. للقيام بذلك ، انظر إلى javap -verbose OverridingInternalExample الذي يمكننا الحصول عليه عن طريق تشغيل javap -verbose OverridingInternalExample . ستسمح لنا المعلمة -verbose بالحصول على رمز بايت أكثر سهولة يتوافق مع برنامج جافا الخاص بنا.

سيظهر الأمر أعلاه قسمين من bytecode.

1. تجمع الثوابت . أنه يحتوي على كل ما هو ضروري لتشغيل البرنامج. على سبيل المثال ، مراجع الأسلوب ( #Methodref ) ، والفصول ( #Class ) ، سلسلة القيم الحرفية ( #String ).



2. الرمز الفرعي للبرنامج. تعليمات bytecode القابلة للتنفيذ.



لماذا تسمى الحمولة الزائدة بطريقة الربط الثابت


في المثال أعلاه ، يعتقد المترجم أنه سيتم استدعاء أسلوب humanMammal.speak() من فئة Mammal ، على الرغم من أنه في وقت التشغيل سيتم استدعاؤه من الكائن المشار إليه في humanMammal - سيكون كائنًا من فئة Human .

عند النظر إلى الكود الخاص بنا ونتائج javap ، نرى أنه يتم استخدام شفرة javap مختلفة لاستدعاء الأساليب humanMammal.speak() و human.speak() و human.speak("Hindi") ، نظرًا لأن المترجم يمكنه التمييز بينها بناءً على مرجع الفصل .

وبالتالي ، في حالة التحميل الزائد للطريقة ، يكون المحول البرمجي قادرًا على تحديد إرشادات كود التعريف وعناوين الطريقة في وقت الترجمة. هذا هو السبب في أن هذا ما يسمى الربط الثابت أو تعدد الأشكال وقت الترجمة.

لماذا تسمى طريقة التجاوز الديناميكي الملزم


لاستدعاء anyMammal.speak() و humanMammal.speak() ، يكون humanMammal.speak() هو نفسه ، لأنه من وجهة نظر المحول البرمجي يتم استدعاء كلا الطريقتين لفئة Mammal :

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

والسؤال المطروح الآن هو ، إذا كان كلا المكالمات لهما نفس الرمز الثنائي ، فكيف تعرف JVM طريقة الاتصال؟

يتم إخفاء الإجابة في bytecode نفسه وفي التعليمات invokevirtual . وفقًا لمواصفات JVM (ملاحظة المترجم: الرجوع إلى JVM spec 2.11.8 ) :
يستدعي تعليمة invokevirtual طريقة المثيل من خلال الإرسال على النوع (الظاهري) للكائن. هذا هو الإرسال العادي للأساليب في لغة برمجة Java.
يستخدم JVM invokevirtual لاستدعاء أساليب Java المكافئة للأساليب الافتراضية C ++. في C ++ ، لتجاوز أسلوب في فئة أخرى ، يجب أن يتم الإعلان عن الطريقة على أنها افتراضية. ولكن في Java ، افتراضيًا ، تكون جميع الطرق افتراضية (باستثناء الطرق النهائية والثابتة) ، لذلك في الفصل الفرعي يمكننا تجاوز أي طريقة.

يأخذ تعليمة invokevirtual مؤشرًا إلى الطريقة المراد استدعاؤها (# 4 هو الفهرس في التجمع الثابت).

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

لكن المرجع رقم 4 يشير أيضًا إلى طريقة وفئة أخرى.

 #4 = Methodref #2.#27 // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V #2 = Class #25 // org/programming/mitra/exercises/OverridingInternalExample$Mammal #25 = Utf8 org/programming/mitra/exercises/OverridingInternalExample$Mammal #27 = NameAndType #35:#17 // speak:()V #35 = Utf8 speak #17 = Utf8 ()V 

يتم استخدام كل هذه الروابط معًا للحصول على مرجع للطريقة والفئة التي توجد بها الطريقة المطلوبة. تم ذكر ذلك أيضًا في مواصفات JVM ( ملاحظة المترجم: الإشارة إلى JVM spec 2.7 ):
لا يتطلب Java Virtual Machine أي بنية داخلية محددة للكائنات.
في بعض تطبيقات Java Virtual Machine بواسطة Oracle ، فإن الإشارة إلى مثيل لفئة ما هي إشارة إلى معالج ، يتكون في حد ذاته من زوج من الارتباطات: يشير أحدهما إلى جدول أساليب الكائن ومؤشر إلى كائن Class يمثل نوع الكائن ، والآخر إلى المنطقة البيانات الموجودة على الكومة التي تحتوي على بيانات الكائن.

هذا يعني أن كل متغير مرجع يحتوي على مؤشرين مخفيين:

  1. مؤشر إلى جدول يحتوي على أساليب الكائن ومؤشر إلى كائن Class ، على سبيل المثال ، [speak(), speak(String) Class object]
  2. مؤشر إلى الذاكرة على الكومة المخصصة لبيانات الكائن ، مثل قيم حقل الكائن.

ولكن مرة أخرى ، يطرح السؤال: كيف يعمل invokevirtual مع هذا؟ لسوء الحظ ، لا يمكن لأحد الإجابة على هذا السؤال ، لأنه يعتمد على تنفيذ JVM ويختلف من JVM إلى JVM.

من المنطق أعلاه ، يمكننا أن نستنتج أن الإشارة إلى كائن يحتوي بشكل غير مباشر على رابط / مؤشر إلى جدول يحتوي على جميع الإشارات إلى طرق هذا الكائن. استعار Java هذا المفهوم من C ++. يُعرف هذا الجدول بأسماء متعددة ، مثل جدول الطريقة الافتراضية (VMT) ، وجدول الوظائف الافتراضية (vftable) ، والجدول الظاهري (vtable) ، وجدول الإرسال .

لا يمكننا التأكد من تطبيق vtable في Java ، لأنه يعتمد على JVM المحدد. ولكن يمكننا أن نتوقع أن تكون الإستراتيجية كما هي في C ++ ، حيث vtable عبارة عن بنية تشبه الصفيف تحتوي على أسماء الأساليب والمراجع الخاصة بها. كلما حاولت JVM تنفيذ طريقة افتراضية ، فإنها تطلب عنوانها في vtable.

لكل فئة ، هناك vtable واحد فقط ، مما يعني أن الجدول فريد ونفس الشيء بالنسبة لجميع كائنات الفئة ، مثل كائن Class. تتم مناقشة كائنات الفصل بمزيد من التفصيل في المقالات. لماذا لا يمكن أن تكون فئة Java الخارجية ثابتة ولماذا Java هي لغة موجهة نحو كائن بحت أو لماذا لا .

وبالتالي ، لا يوجد سوى vtable واحد لفئة Object ، والذي يحتوي على جميع الطرق الـ 11 (إذا لم registerNatives مراعاة registerNatives الأصلي) والارتباطات المقابلة لتطبيقها.



عندما يقوم JVM بتحميل فئة Mammal في الذاكرة ، فإنه ينشئ كائن Class له وينشئ vtable يحتوي على جميع الأساليب من vtable للفئة Object مع المراجع نفسها (لأن Mammal لا يتجاوز الأساليب من Object ) ويضيف إدخالًا جديدًا لأسلوب speak() .



بعد ذلك يأتي فصل فئة Human ، وتقوم JVM بنسخ جميع الإدخالات من vtable للفئة Mammal إلى vtable للفئة Human وتضيف إدخالًا جديدًا للإصدار المفرط من speak(String) .

يعرف JVM أن فئة Human قد تخطت طريقتين: toString() من Object speak() من Mammal . الآن بالنسبة لهذه الطرق ، بدلاً من إنشاء سجلات جديدة تحتوي على روابط محدثة ، ستقوم JVM بتغيير الارتباطات إلى الطرق الموجودة في الفهرس نفسه الذي كانت موجودة فيه سابقًا ، وتحتفظ بنفس أسماء الطرق.



يؤدي تعليمة invokevirtual قيام JVM بمعالجة القيمة في المرجع إلى الطريقة رقم 4 ليس كعنوان ، ولكن كاسم للطريقة التي يجب البحث عنها في vtable للكائن الحالي.
آمل أن يكون الأمر أكثر وضوحًا الآن كيف تستخدم JVM التجمع المستمر وطاولة الطريقة الافتراضية لتحديد الطريقة التي يجب الاتصال بها.
يمكنك العثور على نموذج التعليمة البرمجية في مستودع جيثب .

Source: https://habr.com/ru/post/ar467197/


All Articles