يتطلب تنفيذ برنامج Java خطوتين: التجميع والتنفيذ (التفسير). وفي الوقت نفسه، تعد Java لغة برمجة موجهة للكائنات. عندما يكون للفئة الفرعية والفئة الأصلية نفس الطريقة، وتتجاوز الفئة الفرعية طريقة الفئة الأصلية، عندما يستدعي البرنامج الطريقة في وقت التشغيل، هل يجب عليه استدعاء طريقة الفئة الأصلية أو طريقة الفئة الفرعية التي تم تجاوزها؟ يجب أن يكون السؤال عندما نتعلم مشاكل Java لأول مرة. هنا أولاً سنحدد الطريقة التي سيتم استدعاءها أو تشغيل المتغيرات يسمى الربط.
هناك طريقتان للربط في Java، إحداهما ربط ثابت، وتسمى أيضًا الربط المبكر. والآخر هو الربط الديناميكي، المعروف أيضًا باسم الربط المتأخر.
مقارنة الفرق
1. يحدث الربط الثابت في وقت الترجمة، ويحدث الربط الديناميكي في وقت التشغيل.
2. استخدم المتغيرات أو الأساليب المعدلة باستخدام خاص أو ثابت أو نهائي، واستخدم الربط الثابت. سيتم ربط الأساليب الافتراضية (الطرق التي يمكن تجاوزها بواسطة الفئات الفرعية) ديناميكيًا بناءً على كائن وقت التشغيل.
3. يتم إكمال الربط الثابت باستخدام معلومات الفئة، بينما يجب إكمال الربط الديناميكي باستخدام معلومات الكائن.
4. تكتمل طريقة التحميل الزائد باستخدام الربط الثابت، بينما تكتمل طريقة التجاوز باستخدام الربط الديناميكي.
مثال على طريقة التحميل الزائد
فيما يلي مثال على الأساليب المحملة بشكل زائد.
انسخ رمز الكود كما يلي:
الطبقة العامة TestMain {
public static void main(String[] args) {
سلسلة str = سلسلة جديدة ()؛
المتصل المتصل = المتصل الجديد ()؛
caller.call(str);
}
المتصل فئة ثابتة {
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في المتصل");
}
مكالمة باطلة عامة (سلسلة str) {
System.out.println("مثيل سلسلة موجود في المتصل");
}
}
}
نتيجة التنفيذ هي
انسخ رمز الكود كما يلي:
22:19 $javaTestMain
مثيل سلسلة في المتصل
في الكود أعلاه، يوجد تطبيقان مثقلان لأسلوب الاستدعاء، أحدهما يتلقى كائنًا من النوع Object كمعلمة، والآخر يتلقى كائنًا من النوع String كمعلمة. str هو كائن سلسلة، وسيتم استدعاء كافة أساليب الاتصال التي تتلقى معلمات نوع السلسلة. الربط هنا هو ربط ثابت يعتمد على نوع المعلمة في وقت الترجمة.
يؤكد
مجرد النظر إلى المظهر لا يمكن أن يثبت أنه تم تنفيذ الربط الثابت، يمكنك التحقق منه باستخدام javap لتجميعه.
انسخ رمز الكود كما يلي:
22:19 $ javap -c TestMain
تم تجميعها من "TestMain.java"
الطبقة العامة TestMain {
اختبار رئيسي () عام؛
شفرة:
0: التحميل_0
1: استدعاء خاص #1 // طريقة java/lang/Object."<init>":()V
4: العودة
public static void main(java.lang.String[]);
شفرة:
0: الجديد رقم 2 // فئة java/lang/String
3: مزدوج
4: استدعاء خاص #3 // طريقة java/lang/String."<init>":()V
7:المتجر_1
8: الجديد رقم 4 // فئة TestMain$Caller
11: دوب
12: استدعاء خاص #5 // طريقة TestMain$Caller."<init>":()V
15: أستور_2
16: التحميل_2
17: التحميل_1
18: استدعاء افتراضي #6 // طريقة TestMain$Caller.call:(Ljava/lang/String;)V
21: العودة
}
رأيت هذا السطر 18: استدعاء افتراضي #6 // Method TestMain$Caller.call:(Ljava/lang/String;)V مرتبط بالفعل بشكل ثابت، مما يؤكد أن طريقة المتصل التي تتلقى كائن سلسلة كمعلمة تسمى.
مثال على تجاوز الطريقة
انسخ رمز الكود كما يلي:
الطبقة العامة TestMain {
public static void main(String[] args) {
سلسلة str = سلسلة جديدة ()؛
المتصل المتصل = SubCaller الجديد () ؛
caller.call(str);
}
المتصل فئة ثابتة {
مكالمة باطلة عامة (سلسلة str) {
System.out.println("مثيل سلسلة في المتصل");
}
}
فئة ثابتة SubCaller تمتد المتصل {
@تجاوز
مكالمة باطلة عامة (سلسلة str) {
System.out.println("مثيل سلسلة في SubCaller");
}
}
}
نتيجة التنفيذ هي
انسخ رمز الكود كما يلي:
22:27 $javaTestMain
مثيل سلسلة في SubCaller
في الكود أعلاه، يوجد تطبيق لطريقة الاتصال في Caller. يرث SubCaller المتصل ويعيد كتابة تنفيذ طريقة الاتصال. لقد أعلنا عن متغير callerSub من النوع Caller، لكن هذا المتغير يشير إلى كائن SubCaller. وفقًا للنتائج، يمكن ملاحظة أنه يستدعي تنفيذ طريقة الاتصال لـ SubCaller بدلاً من طريقة الاتصال الخاصة بـ Caller. سبب هذه النتيجة هو أن الربط الديناميكي يحدث في وقت التشغيل، ومن الضروري أثناء عملية الربط تحديد إصدار تطبيق أسلوب الاستدعاء المطلوب الاتصال به.
يؤكد
لا يمكن التحقق من الربط الديناميكي مباشرة باستخدام javap، وإذا ثبت عدم تنفيذ الربط الثابت، فهذا يعني أنه تم تنفيذ الربط الديناميكي.
انسخ رمز الكود كما يلي:
22:27 $ javap -c TestMain
تم تجميعها من "TestMain.java"
الطبقة العامة TestMain {
اختبار رئيسي () عام؛
شفرة:
0: التحميل_0
1: استدعاء خاص #1 // طريقة java/lang/Object."<init>":()V
4: العودة
public static void main(java.lang.String[]);
شفرة:
0: الجديد رقم 2 // فئة java/lang/String
3: مزدوج
4: استدعاء خاص #3 // طريقة java/lang/String."<init>":()V
7:المتجر_1
8: الجديد رقم 4 // فئة TestMain$SubCaller
11: دوب
12: استدعاء خاص #5 // طريقة TestMain$SubCaller."<init>":()V
15: أستور_2
16: التحميل_2
17: التحميل_1
18: استدعاء افتراضي #6 // طريقة TestMain$Caller.call:(Ljava/lang/String;)V
21: العودة
}
كنتيجة أعلاه، 18: استدعاء افتراضي #6 // الطريقة TestMain$Caller.call:(Ljava/lang/String;)V هذا هو TestMain$Caller.call بدلاً من TestMain$SubCaller.call، لأنه لا يمكن تحديد روتين الاتصال الفرعي في وقت الترجمة، لا يزال الفصل عبارة عن تطبيق للفئة الأصلية، لذلك لا يمكن التعامل معه إلا عن طريق الربط الديناميكي في وقت التشغيل.
عندما تجتمع إعادة التحميل مع إعادة الكتابة
المثال التالي غير طبيعي بعض الشيء، هناك نوعان من التحميل الزائد لأسلوب الاتصال في فئة المتصل، والأمر الأكثر تعقيدًا هو أن المتصل الفرعي يدمج المتصل ويتجاوز هاتين الطريقتين. في الواقع، هذا الوضع هو حالة مركبة من الحالتين المذكورتين أعلاه.
ستقوم التعليمة البرمجية التالية أولاً بتنفيذ الربط الثابت لتحديد أسلوب الاستدعاء الذي تكون معلمته عبارة عن كائن سلسلة، ثم إجراء الربط الديناميكي في وقت التشغيل لتحديد ما إذا كان سيتم تنفيذ تنفيذ الاستدعاء للفئة الفرعية أو الفئة الأصل.
انسخ رمز الكود كما يلي:
الطبقة العامة TestMain {
public static void main(String[] args) {
سلسلة str = سلسلة جديدة ()؛
Caller callerSub = new SubCaller();
callerSub.call(str);
}
المتصل فئة ثابتة {
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في المتصل");
}
مكالمة باطلة عامة (سلسلة str) {
System.out.println("مثيل سلسلة موجود في المتصل");
}
}
فئة ثابتة SubCaller تمتد المتصل {
@تجاوز
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في SubCaller");
}
@تجاوز
مكالمة باطلة عامة (سلسلة str) {
System.out.println("مثيل سلسلة في SubCaller");
}
}
}
نتيجة التنفيذ هي
انسخ رمز الكود كما يلي:
22:30 $javaTestMain
مثيل سلسلة في SubCaller
يؤكد
منذ أن تم تقديمه أعلاه، سأقوم بنشر نتائج فك التجميع هنا فقط.
انسخ رمز الكود كما يلي:
22:30 $ javap -c TestMain
تم تجميعها من "TestMain.java"
الطبقة العامة TestMain {
اختبار رئيسي () عام؛
شفرة:
0: التحميل_0
1: استدعاء خاص #1 // طريقة java/lang/Object."<init>":()V
4: العودة
public static void main(java.lang.String[]);
شفرة:
0: الجديد رقم 2 // فئة java/lang/String
3: مزدوج
4: استدعاء خاص #3 // طريقة java/lang/String."<init>":()V
7:المتجر_1
8: الجديد رقم 4 // فئة TestMain$SubCaller
11: دوب
12: استدعاء خاص #5 // طريقة TestMain$SubCaller."<init>":()V
15: أستور_2
16: التحميل_2
17: التحميل_1
18: استدعاء افتراضي #6 // طريقة TestMain$Caller.call:(Ljava/lang/String;)V
21: العودة
}
أسئلة غريبة
أليس من الممكن استخدام الربط الديناميكي؟
في الواقع، من الناحية النظرية، يمكن أيضًا تحقيق ربط طرق معينة عن طريق الربط الثابت. على سبيل المثال:
انسخ رمز الكود كما يلي:
public static void main(String[] args) {
سلسلة str = سلسلة جديدة ()؛
المتصل النهائي callerSub = new SubCaller();
callerSub.call(str);
}
على سبيل المثال، يحتفظ callerSub هنا بكائن subCaller ويكون متغير callerSub نهائيًا، ويتم تنفيذ طريقة الاتصال على الفور. من الناحية النظرية، يمكن للمترجم معرفة أنه يجب استدعاء طريقة الاتصال الخاصة بـ SubCaller من خلال التحليل الكافي للكود.
ولكن لماذا لا يوجد ربط ثابت؟
افترض أن المتصل لدينا يرث من فئة BaseCaller لإطار عمل معين، والذي ينفذ طريقة الاتصال، ويرث BaseCaller من SuperCaller. يتم تطبيق طريقة الاتصال أيضًا في SuperCaller.
افترض أن BaseCaller وSuperCaller في إطار عمل معين 1.0
انسخ رمز الكود كما يلي:
فئة ثابتة SuperCaller {
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في SuperCaller");
}
}
فئة ثابتة BaseCaller تمتد SuperCaller {
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في BaseCaller");
}
}
قمنا بتنفيذ هذا باستخدام الإطار 1.0. يرث المتصل من BaseCaller ويستدعي التابع super.call .
انسخ رمز الكود كما يلي:
الطبقة العامة TestMain {
public static void main(String[] args) {
Object obj = new Object();
SuperCaller callerSub = new SubCaller();
callerSub.call(obj);
}
المتصل فئة ثابتة يمتد BaseCaller {
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في المتصل");
super.call(obj);
}
مكالمة باطلة عامة (سلسلة str) {
System.out.println("مثيل سلسلة موجود في المتصل");
}
}
فئة ثابتة SubCaller تمتد المتصل {
@تجاوز
استدعاء باطلة عامة (كائن كائن) {
System.out.println("مثيل كائن في SubCaller");
}
@تجاوز
استدعاء باطلة عامة (سلسلة سلسلة) {
System.out.println("مثيل سلسلة في SubCaller");
}
}
}
ثم قمنا بتجميع ملف الفئة بناءً على الإصدار 1.0 من إطار العمل هذا، بافتراض أن الربط الثابت يمكن أن يحدد أن super.call الخاص بالمتصل أعلاه قد تم تنفيذه كـ BaseCaller.call.
ثم نفترض مرة أخرى أن BaseCaller لا يعيد كتابة طريقة الاتصال الخاصة بـ SuperCaller في الإصدار 1.1 من هذا الإطار، ثم الافتراض أعلاه هو أن تنفيذ الاتصال الذي يمكن ربطه بشكل ثابت سوف يسبب مشاكل في الإصدار 1.1، لأن super.call يجب أن يستخدم SuperCall في الإصدار. 1.1. تنفيذ طريقة الاتصال، بدلاً من افتراض أن تنفيذ طريقة الاتصال الخاصة بـ BaseCaller يتم تحديده من خلال الربط الثابت.
ولذلك، فإن بعض الأشياء التي يمكن ربطها فعليًا بشكل ثابت يتم ربطها ديناميكيًا مع مراعاة الأمان والاتساق.
الحصول على الإلهام الأمثل؟
نظرًا لأن الربط الديناميكي يحتاج إلى تحديد إصدار تنفيذ الطريقة أو المتغير المطلوب تنفيذه في وقت التشغيل، فهو يستغرق وقتًا أطول من الربط الثابت.
لذلك، دون التأثير على التصميم العام، يمكننا التفكير في تعديل الأساليب أو المتغيرات باستخدام خاص أو ثابت أو نهائي.