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

محو الأنواع (Type Erasure)

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

محو الأنواع (Type Erasure)

الجينيريكس في Java هي ميزة وقت الترجمة فحسب. بعد أن يتحقّق المُترجم من سلامة الأنواع في كودك، يقوم بـإزالة جميع معلومات الأنواع الجينيريكية من الـ bytecode. تُسمّى هذه العملية محو الأنواع (type erasure). فهم هذه الآلية يشرح كثيرًا من السلوكيات "المفاجئة" في الجينيريكس — من قيود instanceof في وقت التشغيل إلى وجود bridge methods.

ما الذي يحدث فعليًا أثناء الترجمة؟

عند كتابة كلاس جينيريكي، يقوم المُترجم بشيئين: يتحقّق من الأنواع، ثم يُعيد كتابة الـ bytecode باستخدام raw type. إذا كان مُعامل النوع غير مقيَّد، يُستبدل بـ Object؛ وإذا كان له قيد (مثل T extends Comparable<T>) يُستبدل بالقيد الأول من اليسار.

// الكود الذي تكتبه public class Box<T> { private T value; public Box(T value) { this.value = value; } public T getValue() { return value; } } // ما يُصدره المُترجم (raw type — T تُمحى وتصبح Object) public class Box { private Object value; public Box(Object value) { this.value = value; } public Object getValue() { return value; } }

يضيف المُترجم أيضًا تحويلات نوع (casts) غير محقَّقة في كل موقع استدعاء تقرأ فيه قيمة مُكتَّبة، فيحدث الـ cast مرة واحدة في كودك وليس في أعماق المكتبة.

Box<String> box = new Box<>("hello"); String s = box.getValue(); // تُترجم إلى: String s = (String) box.getValue();
الفكرة الأساسية: 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 موجود.
// خطأ ترجمة: لا يمكن إنشاء مصفوفة جينيريكية // T[] arr = new T[10]; // الحلّ البديل: مرّر رمز الكلاس واستخدم Array.newInstance public static <T> T[] makeArray(Class<T> type, int size) { @SuppressWarnings("unchecked") T[] arr = (T[]) java.lang.reflect.Array.newInstance(type, size); return arr; }

Bridge Methods

يخلق المحو مشكلة خفية في الوراثة. تأمّل هذه الواجهة الجينيريكية:

public interface Processor<T> { void process(T item); } // بعد المحو تصبح الواجهة: // void process(Object item);

الآن تكتب تطبيقًا محدَّدًا:

public class StringProcessor implements Processor<String> { @Override public void process(String item) { System.out.println(item.toUpperCase()); } }

التابع الذي كتبته له التوقيع process(String)، لكن الواجهة الممحوة تطلب process(Object). هذان توقيعان مختلفان — سينكسر تعدّد الأشكال (polymorphism). لإصلاح ذلك، يولّد المُترجم تلقائيًا bridge method:

// bridge method مُولَّد بواسطة المُترجم (لا تكتبه أنت أبدًا) public void process(Object item) { process((String) item); // يُفوَّض إلى تابعك الحقيقي }

يمكنك رؤية bridge methods في الـ bytecode باستخدام javap -v StringProcessor — تظهر بعلامتَي ACC_BRIDGE وACC_SYNTHETIC. إنّها غير مرئية في الكود المصدري لكنها توابع حقيقية في ملف .class.

لماذا يهمّ هذا؟ إذا استخدمت الـ reflection لسرد توابع كلاس ما، ستظهر bridge methods في القائمة. استبعدها دائمًا بـ method.isBridge() إذا كنت تبني إطار عمل أو معالج annotations.

تلوّث الـ Heap وتحذيرات الكاست غير المحقَّق

يحدث تلوّث الـ heap عندما تُشير متغيّرة من نوع مُقيَّد بالمعاملات إلى كائن ليس من ذلك النوع. يمكن أن يحدث ذلك عند مزج الأنواع الخام مع الجينيريكس، أو عند استخدام @SuppressWarnings("unchecked") بتهوّر.

List<String> strings = new ArrayList<>(); List rawList = strings; // raw type — لا تحذير هنا rawList.add(42); // يُدرج Integer — المُترجم يُحذِّر String s = strings.get(0); // ClassCastException في وقت التشغيل!

يفشل الـ cast الذي أدرجه المُترجم عند استدعاء get، وليس حيث أُضيف الـ 42 — مما يجعل تحديد مصدر الخطأ أصعب. لهذا يجب عدم مزج الأنواع الخام والأنواع المُقيَّدة بالمعاملات في الكود الجديد.

تعامل مع كل تحذير unchecked بجدية. المُترجم يخبرك بأن المحو يعني عدم تمكّنه من ضمان سلامة الأنواع في تلك النقطة. إن أسكتّ التحذير، أضف تعليقًا يشرح سبب أمان هذا الـ cast فعليًا.

محو الأنواع مع القيود (Bounded Erasure)

حين يكون لمُعامل النوع قيد، يصبح النوع الممحو هو القيد نفسه، وليس Object:

// الكود المصدري public <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; } // بعد المحو (T تُستبدل بـ Comparable) public Comparable max(Comparable a, Comparable b) { return a.compareTo(b) >= 0 ? a : b; }

هذا مهم لأنه يعني أن المُترجم يمكنه التحقّق من أن الدوال المعرَّفة على القيد — مثل compareTo — استدعاءات صحيحة حتى بعد المحو.

الخلاصة

محو الأنواع هو حل Java للتوافق مع الإصدارات السابقة: أُضيفت الجينيريكس في Java 5 دون تغيير الـ JVM، لذا توجد كل المعلومات الجينيريكية فقط في المُترجم. يرى وقت التشغيل الأنواع الخام؛ يُدرج المُترجم الـ casts ويولّد bridge methods لإبقاء كل شيء يعمل. فهم المحو يمنع مفاجآت وقت التشغيل ويساعدك على قراءة رسائل الأخطاء ومخرجات الـ reflection بثقة.