الوراثة وتعدّد الأشكال

تعدد الأشكال والإرسال الديناميكي

15 دقيقة الدرس 5 من 14

تعدد الأشكال والإرسال الديناميكي

تعلّمت حتى الآن كيفية إنشاء الصفوف الفرعية وتجاوز التوابع. الآن تأتي الفكرة التي تجمع كل ذلك معًا: تعدد الأشكال — قدرة متغيّر واحد على التصرف كأنه كائنات من أنواع مختلفة. مقترنًا بـالإرسال الديناميكي، وهو الطريقة التي تقرر بها Java وقت التشغيل أيّ نسخة من التابع المُتجاوَز ستستدعي، يصبح تعدد الأشكال العمود الفقري للتصميم المرن والقابل للتوسع.

مرجع من النوع الأب لكائن من النوع الابن

في Java، يمكن لمتغيّر من نوع الصف الأب أن يحمل مرجعًا لأي كائن ابن. هذا مشروع تمامًا لأن الابن هو نوع من الأب:

class Animal { public void speak() { System.out.println("..."); } } class Dog extends Animal { @Override public void speak() { System.out.println("Woof!"); } } class Cat extends Animal { @Override public void speak() { System.out.println("Meow!"); } } public class Main { public static void main(String[] args) { Animal a = new Dog(); // مرجع من النوع الأب، كائن من النوع الابن a.speak(); // تطبع: Woof! a = new Cat(); // نفس المتغيّر، كائن مختلف a.speak(); // تطبع: Meow! } }

المتغيّر a مُعلَن من نوع Animal، لكنه يشير إلى كائن Dog. عند استدعاء speak()، لا تستخدم Java نسخة Animal — بل تستخدم نسخة الكائن الفعلي. هذا القرار يحدث وقت التشغيل، لا وقت الترجمة. هذا هو الإرسال الديناميكي.

الإرسال الديناميكي في جملة واحدة: تنظر Java إلى النوع الفعلي للكائن في الذاكرة لتقرر أيّ تابع متجاوَز تشغّل، بصرف النظر عن النوع المُعلَن للمتغيّر الذي يحمله.

لماذا يهمّنا هذا؟ البرمجة وفق النوع

تظهر القوة الحقيقية حين تكتب كودًا يعمل مع النوع الأب، وبالتالي يعمل مع أي صف ابن حالي أو مستقبلي. يُسمّى هذا المبدأ البرمجة وفق النوع (ويُعبَّر عنه أحيانًا بـ "برمجِ وفق الواجهة لا وفق التنفيذ").

public class ZooKeeper { // هذه الدالة تقبل أي حيوان — Dog أو Cat أو Lion أو أي شيء يُضاف لاحقًا public void makeAnimalSpeak(Animal animal) { animal.speak(); // الإرسال الديناميكي يختار التابع الصحيح } } public class Main { public static void main(String[] args) { ZooKeeper keeper = new ZooKeeper(); keeper.makeAnimalSpeak(new Dog()); // Woof! keeper.makeAnimalSpeak(new Cat()); // Meow! } }

كُتبت makeAnimalSpeak مرة واحدة ولن تحتاج أبدًا للتغيير، حتى لو أضفت صف Lion غدًا. يكفي أن يتجاوز الصف الجديد speak()، والإرسال الديناميكي يتولى الباقي.

تعدد الأشكال مع المصفوفات والقوائم

تخزين كائنات ابن مختلطة تحت نوع أب مشترك في مجموعة هو نمط كلاسيكي:

import java.util.List; import java.util.ArrayList; public class Main { public static void main(String[] args) { List<Animal> animals = new ArrayList<>(); animals.add(new Dog()); animals.add(new Cat()); animals.add(new Dog()); for (Animal a : animals) { a.speak(); // كل كائن يستجيب بنسخته الخاصة } } }

الحلقة لا تحتاج لمعرفة النوع الملموس أو الاكتراث به. الناتج هو:

Woof! Meow! Woof!
صرّح عن المتغيّرات بأعلى نوع مفيد. فضّل Animal a = new Dog() على Dog a = new Dog() حين لا يحتاج بقية الكود سوى سلوك Animal. هذا يتيح لك استبدال النوع الملموس لاحقًا دون تغيير كل سطر يستخدم a.

ما لا يشمله الإرسال الديناميكي

ينطبق الإرسال الديناميكي على توابع النسخة فقط. لا ينطبق على:

  • الحقول — إن أعلن الأب والابن كلاهما حقلًا بالاسم ذاته، فنوع المرجع هو الذي يحدد أي حقل يُقرأ، لا نوع الكائن.
  • التوابع الثابتة (static) — تنتمي التوابع الثابتة للصف لا للكائنات، لذا تُحسم وقت الترجمة بناءً على نوع المرجع.
تجنّب إخفاء التوابع الثابتة. إعلان تابع ثابت في صف ابن بتوقيع مطابق لتابع في الأب يُسمّى إخفاء التابع، لا تجاوزه. لا ينطبق عليه الإرسال الديناميكي، والنتيجة تعتمد على نوع المرجع — مصدر شائع لأخطاء خفية.

مثال ملموس: مساحة الأشكال الهندسية

إليك مثالًا متكاملًا يمكنك تجميعه وتشغيله، يوضح كيف يختار الإرسال الديناميكي التابع area() الصحيح وقت التشغيل:

class Shape { public double area() { return 0.0; } } class Circle extends Shape { private double radius; Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } class Rectangle extends Shape { private double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } public class Main { public static void printArea(Shape s) { System.out.printf("Area = %.2f%n", s.area()); } public static void main(String[] args) { printArea(new Circle(5)); // Area = 78.54 printArea(new Rectangle(4, 6)); // Area = 24.00 } }

كُتبت printArea وفق نوع Shape. ستعمل بشكل صحيح مع كل صف فرعي من Shape قد يُنشأ في المستقبل، دون أي تعديل.

الخلاصة

  • يمكن لمتغيّر من نوع الأب أن يحمل مرجعًا لأي كائن ابن.
  • الإرسال الديناميكي يضمن استدعاء التابع المتجاوَز للكائن الفعلي وقت التشغيل.
  • البرمجة وفق النوع تعني كتابة الكود وفق النوع الأب ليعمل مع جميع الأبناء — الحاليين والمستقبليين.
  • ينطبق الإرسال الديناميكي على توابع النسخة فقط، لا على الحقول ولا على التوابع الثابتة.