الوراثة هي إعادة استخدام بنمط "is-a"
تتيح الوراثة لفئة جديدة أن تُبنى على فئة موجودة. الفئة الجديدة -الفئة الفرعية- تحصل تلقائيًا على حقول ودوال الفئة الأب، ثم تضيف أو تغيّر ما تحتاجه. تلجأ إليها عندما يكون نوع ما صنفًا أكثر تخصيصًا من نوع آخر: فالفئة Dog هي Animal، والفئة SavingsAccount هي BankAccount.
تكتبها باستخدام الكلمة المفتاحية extends. كل ما هو public أو protected في الفئة الأب يصبح متاحًا في الفئة الابن دون إعادة كتابته.
لم تُعلِن الفئة Dog قطّ عن name ولا عن eat()، ومع ذلك تملك كليهما. هذا هو جوهر الفكرة كلها: السلوك المشترك يعيش في مكان واحد.
super: الوصول إلى الفئة الأب
يجب أن يضمن باني الفئة الفرعية أن الفئة الأب تُهيَّأ أولًا. تفعل ذلك بـ super(...)، التي تستدعي باني الفئة الأب ويجب أن تكون أول عبارة في باني الفئة الفرعية. إن أغفلتها، تُدرج جافا ضمنيًا استدعاءً لباني الفئة الأب الذي لا يأخذ معاملات - وإذا لم يكن لدى الفئة الأب مثل هذا الباني، فلن تُترجَم الشيفرة.
يُظهر الخرج أن باني الفئة الأب يعمل قبل جسم الفئة الابن - فالبناء يتدفق من قمة التسلسل الهرمي نزولًا.
إعادة تعريف الدوال
تستطيع الفئة الفرعية استبدال دالة موروثة بإعادة تعريفها بنفس التوقيع. هذا هو إعادة التعريف، وينبغي أن تضع عليه دائمًا الوسم @Override. الوسم ليس إلزاميًا، لكنه يجعل المترجم يتحقق من أنك طابقت فعلًا دالةً في الفئة الأب - فهو يلتقط أخطاءً مطبعية مثل tostring() بدلًا من toString()، والتي كانت ستُنشئ بصمت دالةً جديدة تمامًا لولا ذلك.
رغم أن نوع المصفوفة هو Animal، فإن كل عنصر يُشغّل نسخته الخاصة من speak(). تختار جافا الدالة بناءً على الكائن الحقيقي أثناء التشغيل، لا بناءً على النوع المُعلَن للمتغير - وهذا هو أساس تعدد الأشكال.
استدعاء نسخة الفئة الأب باستخدام super.method()
إعادة التعريف لا تستلزم رمي عمل الفئة الأب. استخدم super.method() لتشغيل النسخة الموروثة ثم البناء عليها:
من دون super.، فإن استدعاء log داخل TimestampLogger.log سيستدعي نفسه ويدخل في تكرار لا نهائي. أما super. فتعني صراحةً "نسخة الفئة الأب".
الحقول الموروثة والوصول
ترى الفئة الفرعية أعضاء الفئة الأب من نوع public وprotected، لكنها لا ترى أعضاءها من نوع private. تظل حقول private موجودة في الكائن - تستطيع دوال الفئة الأب نفسها التعامل معها - لكن الفئة الفرعية لا تستطيع الإشارة إليها مباشرةً. استخدم protected عندما تريد أن تصل إليه الفئات الفرعية مع إبقاء العضو مخفيًا عن الشيفرة غير المرتبطة.
class Base {
private int secret; // غير مرئي للفئات الفرعية
protected int shared; // مرئي للفئات الفرعية
}
class Derived extends Base {
void demo() {
shared = 5; // OK
// secret = 5; // خطأ في الترجمة - private ضمن Base
}
}
ولهذا أيضًا كثيرًا ما يتوجب على باني الفئة الفرعية استدعاء super(...): فهو السبيل الوحيد لتهيئة الحالة الخاصة (private) للفئة الأب.
إيقاف الوراثة باستخدام final
أحيانًا ينبغي ألا تُورَّث فئة على الإطلاق - فالفئة String هي final لهذا السبب بالضبط. وسم فئة بأنها final يمنع إنشاء فئات فرعية منها؛ ووسم دالة بأنها final يمنع إعادة تعريفها مع السماح بوراثة الفئة رغم ذلك.
final class Constants { } // لا يمكن اشتقاق فئات فرعية منها
class Config {
final void load() { } // يمكن للفئات الفرعية وراثة Config
// لكن لا يمكنها إعادة تعريف load()
}
الجأ إلى final عندما يجب أن يكون سلوك الفئة مضمونًا وثابتًا عبر البرنامج كله - فهو إشارة مقصودة بمعنى "لا تورّث"، وليس الخيار الافتراضي.
خطأ شائع: فضّل التركيب عندما لا تكون العلاقة "is-a"
الوراثة مُغرية لأنها تعيد استخدام الشيفرة، لكنها تربط الفئة الابن بالفئة الأب ربطًا وثيقًا. إن لم تكن العلاقة "is-a" حقيقية - كأن تكون فئة Car تحتاج صدفةً إلى Engine - فلا تجعل Car extends Engine. السيارة تملك محركًا، وليست محركًا. اعمل على نمذجة ذلك بحقل (تركيب) بدلًا من ذلك:
class Car {
private Engine engine = new Engine(); // Car تملك Engine (HAS-A)
void start() { engine.ignite(); }
}
استخدم الوراثة فقط عندما تكون الفئة الفرعية حقًا صورة متخصصة من الفئة الأب وتريد أن ترث سلوكها وتستبدله في آنٍ معًا.
التالي: الواجهات
تمنحك الوراثة بـ extends فئة أبًا واحدةً وتنفيذًا مشتركًا. لكن الفئة لا يمكنها أن ترث سوى فئة واحدة فقط - فكيف إذًا تمنح فئاتٍ غير مترابطة قدرةً مشتركة؟ لهذا تُوجد الواجهات: عقد تستطيع فئات عديدة تنفيذه، وهو موضوع الصفحة التالية.
الأسئلة الشائعة
ما هي الوراثة في جافا؟
الوراثة تتيح لفئة واحدة (الفئة الفرعية) أن تعيد استخدام حقول ودوال فئة أخرى (الفئة الأب) باستخدام الكلمة المفتاحية extends. تحصل الفئة الفرعية تلقائيًا على أعضاء الفئة الأب من نوع public وprotected، ويمكنها إضافة أعضاء جديدة أو استبدال السلوك الموروث عبر إعادة التعريف. وهي تعبّر عن علاقة "is-a" أي "هو نوع من": فالفئة Dog هي Animal.
ماذا تفعل الكلمة المفتاحية super في جافا؟
super تشير إلى الفئة الأب. فاستخدام super(...) داخل الباني يستدعي باني الفئة الأب (ويجب أن يكون أول عبارة)، بينما super.method() يستدعي نسخة الفئة الأب من دالة قمت بإعادة تعريفها. هذا يتيح للفئة الفرعية أن تبني على منطق الفئة الأب بدلًا من استبداله بالكامل.
ما الفرق بين إعادة التعريف (overriding) والتحميل الزائد (overloading) في جافا؟
إعادة التعريف تعيد تعريف دالة موروثة في فئة فرعية باستخدام نفس التوقيع، مغيّرةً السلوك - ضع عليها الوسم @Override. أما التحميل الزائد فيعرّف عدة دوال بنفس الاسم لكن بقوائم معاملات مختلفة ضمن الفئة نفسها. إعادة التعريف تتعلق بالوراثة والإرسال أثناء التشغيل؛ أما التحميل الزائد فهو مجرد دالتين تتشاركان الاسم نفسه صدفةً.