محو الأنواع (Type Erasure)
محو الأنواع (Type Erasure)
الجينيريكس في Java هي ميزة وقت الترجمة فحسب. بعد أن يتحقّق المُترجم من سلامة الأنواع في كودك، يقوم بـإزالة جميع معلومات الأنواع الجينيريكية من الـ bytecode. تُسمّى هذه العملية محو الأنواع (type erasure). فهم هذه الآلية يشرح كثيرًا من السلوكيات "المفاجئة" في الجينيريكس — من قيود instanceof في وقت التشغيل إلى وجود bridge methods.
ما الذي يحدث فعليًا أثناء الترجمة؟
عند كتابة كلاس جينيريكي، يقوم المُترجم بشيئين: يتحقّق من الأنواع، ثم يُعيد كتابة الـ bytecode باستخدام raw type. إذا كان مُعامل النوع غير مقيَّد، يُستبدل بـ Object؛ وإذا كان له قيد (مثل T extends Comparable<T>) يُستبدل بالقيد الأول من اليسار.
يضيف المُترجم أيضًا تحويلات نوع (casts) غير محقَّقة في كل موقع استدعاء تقرأ فيه قيمة مُكتَّبة، فيحدث الـ cast مرة واحدة في كودك وليس في أعماق المكتبة.
Box<String> وBox<Integer> هما نفس الكلاس في وقت التشغيل. لا يوجد سوى ملف .class واحد — Box.class — وكلا النوعين مثيلان منه.
التبعات: ما لا يمكنك فعله في وقت التشغيل
بما أن مُعاملات النوع تختفي من الـ bytecode، لا يمكنك استخدامها في حالات تتطلّب معلومات النوع في وقت التشغيل:
- لا يمكن إنشاء مصفوفات جينيريكية:
new T[10]غير قانوني لأن الـ JVM لا يعرف ما هوTفي وقت التشغيل. - لا يمكن استخدام
instanceofمع مُعامل نوع:obj instanceof Tلن يُترجم. - لا يمكن فعل
new T(): لن يعرف الـ JVM أي constructor يستدعي. - مُعاملات النوع الجينيريكي ليست reified — لا يمكنك مثلًا كتابة
List<String>.class؛ هذا التعبير غير موجود. فقطList.classموجود.
Bridge Methods
يخلق المحو مشكلة خفية في الوراثة. تأمّل هذه الواجهة الجينيريكية:
الآن تكتب تطبيقًا محدَّدًا:
التابع الذي كتبته له التوقيع process(String)، لكن الواجهة الممحوة تطلب process(Object). هذان توقيعان مختلفان — سينكسر تعدّد الأشكال (polymorphism). لإصلاح ذلك، يولّد المُترجم تلقائيًا bridge method:
يمكنك رؤية bridge methods في الـ bytecode باستخدام javap -v StringProcessor — تظهر بعلامتَي ACC_BRIDGE وACC_SYNTHETIC. إنّها غير مرئية في الكود المصدري لكنها توابع حقيقية في ملف .class.
method.isBridge() إذا كنت تبني إطار عمل أو معالج annotations.
تلوّث الـ Heap وتحذيرات الكاست غير المحقَّق
يحدث تلوّث الـ heap عندما تُشير متغيّرة من نوع مُقيَّد بالمعاملات إلى كائن ليس من ذلك النوع. يمكن أن يحدث ذلك عند مزج الأنواع الخام مع الجينيريكس، أو عند استخدام @SuppressWarnings("unchecked") بتهوّر.
يفشل الـ cast الذي أدرجه المُترجم عند استدعاء get، وليس حيث أُضيف الـ 42 — مما يجعل تحديد مصدر الخطأ أصعب. لهذا يجب عدم مزج الأنواع الخام والأنواع المُقيَّدة بالمعاملات في الكود الجديد.
محو الأنواع مع القيود (Bounded Erasure)
حين يكون لمُعامل النوع قيد، يصبح النوع الممحو هو القيد نفسه، وليس Object:
هذا مهم لأنه يعني أن المُترجم يمكنه التحقّق من أن الدوال المعرَّفة على القيد — مثل compareTo — استدعاءات صحيحة حتى بعد المحو.
الخلاصة
محو الأنواع هو حل Java للتوافق مع الإصدارات السابقة: أُضيفت الجينيريكس في Java 5 دون تغيير الـ JVM، لذا توجد كل المعلومات الجينيريكية فقط في المُترجم. يرى وقت التشغيل الأنواع الخام؛ يُدرج المُترجم الـ casts ويولّد bridge methods لإبقاء كل شيء يعمل. فهم المحو يمنع مفاجآت وقت التشغيل ويساعدك على قراءة رسائل الأخطاء ومخرجات الـ reflection بثقة.