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

قيود الأنواع العامة

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

قيود الأنواع العامة

تجعل الأنواع العامة (Generics) كود Java قابلًا لإعادة الاستخدام وآمنًا من حيث النوع، لكنها تأتي مع مجموعة من القيود الصارمة التي تُربك حتى المطورين ذوي الخبرة. فهم سبب وجود هذه القيود — المتجذّرة في ميزة JVM المعروفة بـ type erasure (محو النوع) — يجعل تذكّرها والتعامل معها أمرًا أيسر بكثير.

مراجعة سريعة: محو النوع

عند وقت الترجمة يتحقق مترجم Java من جميع الأنواع العامة. حين ينتج bytecode يقوم بـ محو معاملات النوع، مستبدلًا إياها بـ Object أو بالحد الأول. في وقت التشغيل، List<String> وList<Integer> كلتاهما مجرد List — فالـ JVM لا يرى فرقًا بينهما. هذا المحو هو السبب الجذري لكل القيود العامة تقريبًا.

القيد الأول — لا يمكن استخدام أنواع بدائية كمعاملات نوع

يجب أن تكون معاملات النوع أنواعًا مرجعية (reference types). لا يمكنك استخدام int أو double أو boolean أو أي نوع بدائي آخر كمعامل نوع.

// لا يُترجم List<int> numbers = new ArrayList<>(); // الصحيح — استخدم فئة التغليف List<Integer> numbers = new ArrayList<>(); numbers.add(42); // autoboxing: int -> Integer تلقائيًا int n = numbers.get(0); // unboxing: Integer -> int تلقائيًا

لماذا؟ بعد المحو تحمل الخانة في القائمة مرجع Object. الأنواع البدائية ليست كائنات ولا يمكن تخزينها كـ Object. أنواع التغليف (Integer، Double، Long، إلخ) كائنات كاملة فتعمل بلا مشكلة. تحوّل Java التلقائي (autoboxing) بينهما بشفافية في معظم الحالات.

ملاحظة أداء: للـ autoboxing تكلفة طفيفة. إن كنت تحتاج تجميعة عالية الأداء من الأنواع البدائية (ملايين العناصر في حلقة مكثفة)، فكّر في مكتبات متخصصة كـ Eclipse Collections أو المصفوفات العادية. للاستخدام اليومي تُعدّ أنواع التغليف مقبولة تمامًا.

القيد الثاني — لا يمكن إنشاء مصفوفات من أنواع معلمجة

لا يمكنك إنشاء مصفوفة يكون نوع عناصرها نوعًا عامًا معلمجًا.

// لا يُترجم List<String>[] table = new List<String>[10]; // الحل الأول — استخدم النوع الخام (يُولّد تحذير unchecked) @SuppressWarnings("unchecked") List<String>[] table = new List[10]; // الحل الثاني — استخدم List من Lists (مُفضَّل) List<List<String>> table = new ArrayList<>();

لماذا؟ مصفوفات Java متغايرة (covariant): فـ String[] هي Object[]. تحمل المصفوفات أيضًا نوع عناصرها في وقت التشغيل وتُجري فحص قابلية التعيين عند كل عملية كتابة (array store check). بعد المحو تصبح List<String>[] وList<Integer>[] كلتاهما List[]. الجمع بين المصفوفات المتغايرة والأنواع الممحوة سيتيح تخزين النوع الخاطئ بصمت، ويمنع المترجم ذلك تفاديًا لخطأ heap pollution الذي لن يظهر إلا لاحقًا كـ ClassCastException غامض.

heap pollution يحدث حين يشير متغير من نوع معلمج إلى كائن ليس من ذلك النوع. الحل الخام أعلاه قد يسبّبه؛ التعليق التوضيحي @SuppressWarnings("unchecked") هو إقرارك بأنك راجعت الكود يدويًا. فضّل List<List<String>> لتجنّب المشكلة كليًا.

القيد الثالث — لا يمكن استخدام instanceof مع أنواع معلمجة

لأن معاملات النوع تُمحى، فلا يملك الـ JVM أي معلومات عنها في وقت التشغيل. لذلك يكون استخدام instanceof مع نوع معلمج أمرًا غير قانوني.

Object obj = getSomeObject(); // لا يُترجم if (obj instanceof List<String>) { ... } // الصحيح — تحقق من النوع الخام، ثم تحويل بحذر if (obj instanceof List<?> list) { // Java 16+: متغير النمط // obj هو نوع ما من List، لكن لا يمكن التأكد من <String> في وقت التشغيل }

صيغة الحرف البديل List<?> مسموح بها لأنها لا تدّعي شيئًا عن معامل النوع — تقول فقط "قائمة ما". أما الصيغة المعلمجة List<String> فتعد بفحص وقت تشغيل لا يستطيع الـ JVM إجراءه بعد المحو.

القيد الرابع — لا يمكن إنشاء نسخة من معامل النوع مباشرة

كتابة new T() داخل فئة عامة أمر غير مسموح لأنه بعد المحو لا يعرف المترجم أي مُنشئ يُستدعى.

class Factory<T> { // لا يُترجم T create() { return new T(); } // الحل — مرّر Supplier أو Class<T> T createWithSupplier(java.util.function.Supplier<T> supplier) { return supplier.get(); } } // الاستخدام Factory<StringBuilder> f = new Factory<>(); StringBuilder sb = f.createWithSupplier(StringBuilder::new);

القيد الخامس — لا يمكن للأعضاء الثابتة استخدام معامل نوع الفئة

الحقول والأساليب الثابتة تنتمي إلى الفئة ذاتها لا إلى أي نسخة معلمجة بعينها. استخدام معامل نوع الفئة في سياق ثابت محظور بالتالي.

class Container<T> { // لا يُترجم — T معامل على مستوى النسخة لا ثابت على مستوى الفئة private static T defaultValue; // لا يُترجم public static T getDefault() { return defaultValue; } // الصحيح — أعلن معامل نوع منفصلًا على الأسلوب الثابت public static <U> U identity(U value) { return value; } }

تجميع الأفكار — فئة أدوات عامة نظيفة

import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; public class GenericBox<T> { private final List<T> items = new ArrayList<>(); // List من Lists مقبول public void add(T item) { items.add(item); } public T get(int index) { return items.get(index); } // مساعد ثابت يستخدم <U> الخاص به لا T الفئة public static <U> GenericBox<U> filled(Supplier<U> supplier, int count) { GenericBox<U> box = new GenericBox<>(); for (int i = 0; i < count; i++) { box.add(supplier.get()); } return box; } }

الخلاصة

  • لا أنواع بدائية كمعاملات نوع — استخدم أنواع التغليف؛ يتولى autoboxing التحويل.
  • لا new T[] — استخدم List<T> أو مرّر مصفوفة خام مع تحويل غير مفحوص.
  • لا instanceof List<String> — تحقق من List<?> بدلًا من ذلك.
  • لا new T() — مرّر Supplier<T> أو Class<T>.
  • لا استخدام ثابت لمعامل نوع الفئة — أعط الأسلوب الثابت معامل نوعه الخاص.

كل واحدة من هذه القيود تعود إلى السبب ذاته: محو النوع. حين تستوعب أن الـ JVM لا يرى في وقت التشغيل إلا الأنواع الخام، تتوقف هذه القواعد عن أن تبدو اعتباطية وتبدأ في أن تكون منطقية تمامًا.