الأنواع العامّة

الجينيريكس والوراثة

15 دقيقة الدرس 9 من 13

الجينيريكس والوراثة

أحد أكثر مصادر الارتباك شيوعًا عند تعلّم الجينيريكس هو ما يحدث حين تجمعها مع تسلسل الفئات الاعتيادي في جافا. أنت تعرف بالفعل أن String يرث من Object. لذلك قد تفترض أن List<String> هو نوع فرعي من List<Object>. لكنه ليس كذلك — وهذا الدرس يشرح السبب بدقة، إلى جانب القواعد الفعلية.

المفاجأة الجوهرية: الثبات (Invariance)

الأنواع الجينيرية في جافا ثابتة (invariant). هذا يعني أنه رغم كون String نوعًا فرعيًا من Object، فإن List<String> ليس نوعًا فرعيًا من List<Object>. إنهما نوعان لا صلة بينهما من وجهة نظر المُترجم.

الكود التالي لا يُترجَم:

List<String> strings = new ArrayList<>(); List<Object> objects = strings; // خطأ في التحويل — incompatible types
الفكرة الأساسية — الثبات: List<String> وList<Object> نوعان متوازيان، لا أحدهما أعلى من الآخر. لا أيٌّ منهما نوع فرعي من الآخر. هذه هي القاعدة الجوهرية للجينيريكس مع الوراثة في جافا.

لماذا الثبات هو الخيار الصحيح؟

هذا القيد موجود لحماية سلامة الأنواع. افترض أن المُترجم كان يسمح بتلك الإسناد. انظر ماذا كان يمكن أن يحدث:

List<String> strings = new ArrayList<>(); strings.add("hello"); // افترض أن هذا كان مسموحًا به (وهو ليس كذلك): List<Object> objects = strings; objects.add(42); // 42 هو Object، يبدو مقبولًا للمُترجم String s = strings.get(1); // ClassCastException وقت التشغيل!

من خلال المرجع objects أدخلت عددًا صحيحًا Integer داخل ما هو فعليًا List<String>. قراءته لاحقًا كـ String ستتسبّب في انهيار البرنامج وقت التشغيل. الثبات يوقف هذه السلسلة بأكملها وقت التحويل، قبل أن يعمل البرنامج أصلًا.

المصفوفات في جافا مقارنةً بالجينيريكس مفيدة هنا: المصفوفات متغيّرة (covariant)، أي أن String[] هو نوع فرعي من Object[]. هذه المرونة تأتي بثمن — يجب على JVM إجراء فحص نوع وقت التشغيل عند كل كتابة في المصفوفة، ويرمي ArrayStoreException حين يفشل الفحص. اختارت الجينيريكس السلامة وقت التحويل بدلًا من التصحيح وقت التشغيل.

حين تحتاج المرونة، استخدم البدل (wildcard). يقبل List<? extends Object> (أو ببساطة List<?>) أي قائمة مُعرَّفة بمُعامل نوع للقراءة فقط. هذا سيُغطَّى في دروس البدل؛ هنا نركّز على فهم السبب الجوهري للقاعدة الأصلية.

ما الذي يمتلك علاقة نوع فرعي فعلًا؟

شيئان يعملان كما هو متوقّع وبشكل آمن:

  • نفس مُعامل النوع، فئة حاوية مختلفة: لأن ArrayList يُنفّذ List، فإن ArrayList<String> هو نوع فرعي من List<String>. كلا الطرفين يستخدمان نفس مُعامل النوع، لا خطر هنا.
  • الأنواع الخام (raw types): List بدون مُعامل نوع هو نوع أعلى من List<String>. هذا يُترجَم، لكنك تخسر سلامة الأنواع وتحصل على تحذيرات — تجنّب الأنواع الخام في الكود الجديد.
// مقبول — نفس مُعامل النوع، فئة حاوية فرعية List<String> list = new ArrayList<String>(); // مقبول — LinkedList أيضًا يُنفّذ List List<Integer> linked = new LinkedList<Integer>(); // تجنّب — النوع الخام يفقد سلامة الأنواع List raw = new ArrayList<String>(); // يُترجَم مع تحذير

الفئات الجينيرية يمكنها توسيع فئات جينيرية أخرى

يمكنك توسيع أو تنفيذ نوع جينيري تمامًا كأي نوع آخر. قاعدة الثبات تمنع فقط التعامل مع إسناد مُعامل واحد على أنه إسناد آخر للجينيريك نفسه. حين تمدّد بمُعامل نوع متطابق أو متوافق، كل شيء مقبول:

// تمرير نفس مُعامل النوع إلى الفئة الأم class NumberList<T extends Number> extends ArrayList<T> { public double sum() { double total = 0; for (T item : this) { total += item.doubleValue(); } return total; } } // الاستخدام NumberList<Integer> ints = new NumberList<>(); ints.add(10); ints.add(20); ints.add(30); System.out.println(ints.sum()); // 60.0 // NumberList<Integer> هو List<Integer> (نفس المُعامل، فئة فرعية — مقبول) List<Integer> view = ints; // NumberList<Integer> ليس List<Number> أو List<Object> — الثبات لا يزال سارياً

تنفيذ واجهة جينيرية بمُعامل نوع ثابت

يمكن لفئة محدّدة تنفيذ واجهة جينيرية وتثبيت مُعامل النوع إلى نوع معيّن. تصبح الفئة المحدّدة حينئذٍ نوعًا فرعيًا من ذلك الإسناد تحديدًا:

interface Repository<T> { void save(T entity); T findById(int id); } class UserRepository implements Repository<String> { private final java.util.Map<Integer, String> store = new java.util.HashMap<>(); @Override public void save(String entity) { store.put(store.size(), entity); } @Override public String findById(int id) { return store.get(id); } } // UserRepository هو Repository<String> — إسناد مقبول Repository<String> repo = new UserRepository(); repo.save("Alice"); System.out.println(repo.findById(0)); // Alice

توحيد الصورة الذهنية

حين ترى نوعًا جينيريًا مثل Container<T>، فكّر في جزء القوسين الزاويَّين كـعلامة تجارية تُثبَّت وقت التحويل. حاويتان تحملان علامتين مختلفتين هما نوعان تمامًا مختلفان بغضّ النظر عن أي علاقة وراثة بين العلامتين أنفسهما. الاستثناء الوحيد هو حين تكون فئة الحاوية نفسها في علاقة وراثة (مثل ArrayList يرث AbstractList ويُنفّذ List) وكلا الطرفين يحملان نفس العلامة.

خطأ شائع: كتابة دالة تقبل List<Object> ثم الاستغراب من عدم قدرتك على تمرير List<String> إليها. الحل هو استخدام بدل بحدّ أعلى: List<? extends Object> — وهذا مُغطَّى في الدروس التالية.

الخلاصة

  • الأنواع الجينيرية في جافا ثابتة (invariant): List<String> ليس نوعًا فرعيًا من List<Object>.
  • الثبات متعمَّد — يمنع فئة من أخطاء ClassCastException وقت التشغيل التي يمكن أن تسبّبها المصفوفات المتغيّرة.
  • الوراثة الاعتيادية تسري حين يكون مُعامل النوع متطابقًا: ArrayList<String> هو نوع فرعي من List<String>.
  • يمكن للفئات الجينيرية توسيع أو تنفيذ أنواع جينيرية أخرى عبر تمرير مُعامل النوع أو تقييده.
  • حين تحتاج مرونة القراءة عبر إسنادات مختلفة، استخدم البدل — موضوع الدرسَين التاليَّين.