قيود الأنواع العامة
قيود الأنواع العامة
تجعل الأنواع العامة (Generics) كود Java قابلًا لإعادة الاستخدام وآمنًا من حيث النوع، لكنها تأتي مع مجموعة من القيود الصارمة التي تُربك حتى المطورين ذوي الخبرة. فهم سبب وجود هذه القيود — المتجذّرة في ميزة JVM المعروفة بـ type erasure (محو النوع) — يجعل تذكّرها والتعامل معها أمرًا أيسر بكثير.
مراجعة سريعة: محو النوع
عند وقت الترجمة يتحقق مترجم Java من جميع الأنواع العامة. حين ينتج bytecode يقوم بـ محو معاملات النوع، مستبدلًا إياها بـ Object أو بالحد الأول. في وقت التشغيل، List<String> وList<Integer> كلتاهما مجرد List — فالـ JVM لا يرى فرقًا بينهما. هذا المحو هو السبب الجذري لكل القيود العامة تقريبًا.
القيد الأول — لا يمكن استخدام أنواع بدائية كمعاملات نوع
يجب أن تكون معاملات النوع أنواعًا مرجعية (reference types). لا يمكنك استخدام int أو double أو boolean أو أي نوع بدائي آخر كمعامل نوع.
لماذا؟ بعد المحو تحمل الخانة في القائمة مرجع Object. الأنواع البدائية ليست كائنات ولا يمكن تخزينها كـ Object. أنواع التغليف (Integer، Double، Long، إلخ) كائنات كاملة فتعمل بلا مشكلة. تحوّل Java التلقائي (autoboxing) بينهما بشفافية في معظم الحالات.
القيد الثاني — لا يمكن إنشاء مصفوفات من أنواع معلمجة
لا يمكنك إنشاء مصفوفة يكون نوع عناصرها نوعًا عامًا معلمجًا.
لماذا؟ مصفوفات Java متغايرة (covariant): فـ String[] هي Object[]. تحمل المصفوفات أيضًا نوع عناصرها في وقت التشغيل وتُجري فحص قابلية التعيين عند كل عملية كتابة (array store check). بعد المحو تصبح List<String>[] وList<Integer>[] كلتاهما List[]. الجمع بين المصفوفات المتغايرة والأنواع الممحوة سيتيح تخزين النوع الخاطئ بصمت، ويمنع المترجم ذلك تفاديًا لخطأ heap pollution الذي لن يظهر إلا لاحقًا كـ ClassCastException غامض.
@SuppressWarnings("unchecked") هو إقرارك بأنك راجعت الكود يدويًا. فضّل List<List<String>> لتجنّب المشكلة كليًا.
القيد الثالث — لا يمكن استخدام instanceof مع أنواع معلمجة
لأن معاملات النوع تُمحى، فلا يملك الـ JVM أي معلومات عنها في وقت التشغيل. لذلك يكون استخدام instanceof مع نوع معلمج أمرًا غير قانوني.
صيغة الحرف البديل List<?> مسموح بها لأنها لا تدّعي شيئًا عن معامل النوع — تقول فقط "قائمة ما". أما الصيغة المعلمجة List<String> فتعد بفحص وقت تشغيل لا يستطيع الـ JVM إجراءه بعد المحو.
القيد الرابع — لا يمكن إنشاء نسخة من معامل النوع مباشرة
كتابة new T() داخل فئة عامة أمر غير مسموح لأنه بعد المحو لا يعرف المترجم أي مُنشئ يُستدعى.
القيد الخامس — لا يمكن للأعضاء الثابتة استخدام معامل نوع الفئة
الحقول والأساليب الثابتة تنتمي إلى الفئة ذاتها لا إلى أي نسخة معلمجة بعينها. استخدام معامل نوع الفئة في سياق ثابت محظور بالتالي.
تجميع الأفكار — فئة أدوات عامة نظيفة
الخلاصة
- لا أنواع بدائية كمعاملات نوع — استخدم أنواع التغليف؛ يتولى autoboxing التحويل.
- لا
new T[]— استخدمList<T>أو مرّر مصفوفة خام مع تحويل غير مفحوص. - لا
instanceof List<String>— تحقق منList<?>بدلًا من ذلك. - لا
new T()— مرّرSupplier<T>أوClass<T>. - لا استخدام ثابت لمعامل نوع الفئة — أعط الأسلوب الثابت معامل نوعه الخاص.
كل واحدة من هذه القيود تعود إلى السبب ذاته: محو النوع. حين تستوعب أن الـ JVM لا يرى في وقت التشغيل إلا الأنواع الخام، تتوقف هذه القواعد عن أن تبدو اعتباطية وتبدأ في أن تكون منطقية تمامًا.