الجينيريكس والوراثة
الجينيريكس والوراثة
أحد أكثر مصادر الارتباك شيوعًا عند تعلّم الجينيريكس هو ما يحدث حين تجمعها مع تسلسل الفئات الاعتيادي في جافا. أنت تعرف بالفعل أن String يرث من Object. لذلك قد تفترض أن List<String> هو نوع فرعي من List<Object>. لكنه ليس كذلك — وهذا الدرس يشرح السبب بدقة، إلى جانب القواعد الفعلية.
المفاجأة الجوهرية: الثبات (Invariance)
الأنواع الجينيرية في جافا ثابتة (invariant). هذا يعني أنه رغم كون String نوعًا فرعيًا من Object، فإن List<String> ليس نوعًا فرعيًا من List<Object>. إنهما نوعان لا صلة بينهما من وجهة نظر المُترجم.
الكود التالي لا يُترجَم:
List<String> وList<Object> نوعان متوازيان، لا أحدهما أعلى من الآخر. لا أيٌّ منهما نوع فرعي من الآخر. هذه هي القاعدة الجوهرية للجينيريكس مع الوراثة في جافا.
لماذا الثبات هو الخيار الصحيح؟
هذا القيد موجود لحماية سلامة الأنواع. افترض أن المُترجم كان يسمح بتلك الإسناد. انظر ماذا كان يمكن أن يحدث:
من خلال المرجع objects أدخلت عددًا صحيحًا Integer داخل ما هو فعليًا List<String>. قراءته لاحقًا كـ String ستتسبّب في انهيار البرنامج وقت التشغيل. الثبات يوقف هذه السلسلة بأكملها وقت التحويل، قبل أن يعمل البرنامج أصلًا.
المصفوفات في جافا مقارنةً بالجينيريكس مفيدة هنا: المصفوفات متغيّرة (covariant)، أي أن String[] هو نوع فرعي من Object[]. هذه المرونة تأتي بثمن — يجب على JVM إجراء فحص نوع وقت التشغيل عند كل كتابة في المصفوفة، ويرمي ArrayStoreException حين يفشل الفحص. اختارت الجينيريكس السلامة وقت التحويل بدلًا من التصحيح وقت التشغيل.
List<? extends Object> (أو ببساطة List<?>) أي قائمة مُعرَّفة بمُعامل نوع للقراءة فقط. هذا سيُغطَّى في دروس البدل؛ هنا نركّز على فهم السبب الجوهري للقاعدة الأصلية.
ما الذي يمتلك علاقة نوع فرعي فعلًا؟
شيئان يعملان كما هو متوقّع وبشكل آمن:
- نفس مُعامل النوع، فئة حاوية مختلفة: لأن
ArrayListيُنفّذList، فإنArrayList<String>هو نوع فرعي منList<String>. كلا الطرفين يستخدمان نفس مُعامل النوع، لا خطر هنا. - الأنواع الخام (raw types):
Listبدون مُعامل نوع هو نوع أعلى منList<String>. هذا يُترجَم، لكنك تخسر سلامة الأنواع وتحصل على تحذيرات — تجنّب الأنواع الخام في الكود الجديد.
الفئات الجينيرية يمكنها توسيع فئات جينيرية أخرى
يمكنك توسيع أو تنفيذ نوع جينيري تمامًا كأي نوع آخر. قاعدة الثبات تمنع فقط التعامل مع إسناد مُعامل واحد على أنه إسناد آخر للجينيريك نفسه. حين تمدّد بمُعامل نوع متطابق أو متوافق، كل شيء مقبول:
تنفيذ واجهة جينيرية بمُعامل نوع ثابت
يمكن لفئة محدّدة تنفيذ واجهة جينيرية وتثبيت مُعامل النوع إلى نوع معيّن. تصبح الفئة المحدّدة حينئذٍ نوعًا فرعيًا من ذلك الإسناد تحديدًا:
توحيد الصورة الذهنية
حين ترى نوعًا جينيريًا مثل Container<T>، فكّر في جزء القوسين الزاويَّين كـعلامة تجارية تُثبَّت وقت التحويل. حاويتان تحملان علامتين مختلفتين هما نوعان تمامًا مختلفان بغضّ النظر عن أي علاقة وراثة بين العلامتين أنفسهما. الاستثناء الوحيد هو حين تكون فئة الحاوية نفسها في علاقة وراثة (مثل ArrayList يرث AbstractList ويُنفّذ List) وكلا الطرفين يحملان نفس العلامة.
List<Object> ثم الاستغراب من عدم قدرتك على تمرير List<String> إليها. الحل هو استخدام بدل بحدّ أعلى: List<? extends Object> — وهذا مُغطَّى في الدروس التالية.
الخلاصة
- الأنواع الجينيرية في جافا ثابتة (invariant):
List<String>ليس نوعًا فرعيًا منList<Object>. - الثبات متعمَّد — يمنع فئة من أخطاء
ClassCastExceptionوقت التشغيل التي يمكن أن تسبّبها المصفوفات المتغيّرة. - الوراثة الاعتيادية تسري حين يكون مُعامل النوع متطابقًا:
ArrayList<String>هو نوع فرعي منList<String>. - يمكن للفئات الجينيرية توسيع أو تنفيذ أنواع جينيرية أخرى عبر تمرير مُعامل النوع أو تقييده.
- حين تحتاج مرونة القراءة عبر إسنادات مختلفة، استخدم البدل — موضوع الدرسَين التاليَّين.