English 🌐
11 الدرس 5 من 5

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

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

البايت كود (Bytecode) T يصبح Object المترجم يُدرج التحويلات (Casts) وقت الترجمة فقط (Compile-Time Only)

1 سؤال رائع من طالب

طالب ذكي يرفع يده ويسأل: "دكتور، إذا كان لدينا Glass<Juice> و Glass<Water> و Glass<Honey>، عندما نترجم البرنامج، كم ملف .class يتم إنشاؤه؟ ثلاثة؟"

هذا سؤال ممتاز! الإجابة قد تفاجئك: ملف واحد فقط. فقط Glass.class. ليس ثلاثة. ليس عشرة. واحد فقط.

لكن كيف يكون ذلك ممكنًا؟ كيف يمكن لملف بايت كود واحد أن يعمل مع Juice و Water و Honey؟ دعني أخبرك بشيء مذهل...

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

التعريف
محو الأنواع (Type Erasure) يعني أن معلومات الأنواع المعممة (Generic Types) موجودة فقط في وقت الترجمة (Compile Time). بعد الترجمة، يتم محو جميع الأنواع المعممة من البايت كود (Bytecode). الـ JVM لا يرى أبدًا <T> الخاص بك -- فهو يختفي تمامًا.

إليك العملية خطوة بخطوة:

  1. تكتب Glass<T> مع معاملات الأنواع (Type Parameters) في الكود المصدري.
  2. المترجم (Compiler) يتحقق من أن جميع الأنواع صحيحة -- يعمل كـمدقق إملائي.
  3. ثم يقوم المترجم بـإزالة جميع معاملات الأنواع من الكود.
  4. يتم استبدال T بـ Object (عندما يكون غير محدود).
  5. يقوم المترجم بـإدراج تحويلات الأنواع (Casts) اللازمة تلقائيًا.
  6. البايت كود الناتج لا يحتوي على أي معلومات عن الأنماط المعممة على الإطلاق.

رسم توضيحي عملية محو الأنواع (Erasure Process)

01
البايت كود (Bytecode)
Glass.class
Object في كل مكان، مع تحويلات مُدرجة
المترجم (Compiler)
فحص الأنواع + المحو
يتحقق من الأنواع ثم يمحوها
</>
الكود المصدري (Source Code)
Glass<T>
تكتب كودًا بأنماط معممة

3 صنف الكأس (Glass) — قبل وبعد المحو

دعنا نرى ما يفعله المترجم فعلًا بصنف Glass<T> المحبوب لدينا. على اليمين ما تكتبه أنت. وعلى اليسار ما يحتويه البايت كود فعلًا بعد محو الأنواع.

الكود المصدري الخاص بك
public class Glass<T> { private T liquid; public Glass(T liquid) { this.liquid = liquid; } public T getLiquid() { return liquid; } public void setLiquid(T liquid) { this.liquid = liquid; } }
بعد محو الأنواع (البايت كود)
public class Glass { private Object liquid; public Glass(Object liquid) { this.liquid = liquid; } public Object getLiquid() { return liquid; } public void setLiquid(Object liquid) { this.liquid = liquid; } }
انظر بعناية!
بعد المحو، تم استبدال كل T بـ Object. هذا هو نفس النهج القديم تمامًا من الدرس 1! لكن انتظر -- هناك فرق جوهري في كيفية استخدام الكود لهذا الصنف...

4 كود الاستخدام — قبل وبعد المحو

السحر الحقيقي ليس في تعريف الصنف -- بل في الاستخدام. شاهد ما يحدث للكود الذي يستخدم الصنف المعمم.

الكود المصدري الخاص بك
Glass<Juice> g1 = new Glass<>(new Juice()); Juice j = g1.getLiquid(); Glass<Water> g2 = new Glass<>(new Water()); Water w = g2.getLiquid();
بعد المحو (البايت كود)
Glass g1 = new Glass(new Juice()); Juice j = (Juice) g1.getLiquid(); Glass g2 = new Glass(new Water()); Water w = (Water) g2.getLiquid();
العمل السري للمترجم
في الكود المصدري -- لا يوجد تحويل أنواع (Casting). في البايت كود -- المترجم يُدرج التحويلات تلقائيًا! لكن النقطة الحاسمة هي: لأن المترجم تحقق بالفعل من جميع الأنواع في وقت الترجمة، فإن هذه التحويلات مضمونة أن تكون صحيحة. المترجم لن يُدرج أبدًا تحويلًا خاطئًا. هذا ما يجعل الأنماط المعممة (Generics) آمنة -- المدقق الإملائي يعمل أولًا، وعندها فقط يحدث المحو.

5 الحيلة العبقرية

الآن ترى الحيلة العبقرية للأنماط المعممة (Generics). إنه نظام أنيق من مرحلتين يمنحك أفضل ما في العالمين:

كيف تعمل الأنماط المعممة (Generics) فعلًا
  1. في وقت الترجمة (Compile Time)، يعمل المترجم كمدقق إملائي -- يتحقق من أن كل نوع صحيح.
  2. بعد التحقق، يمحو جميع معاملات الأنواع (T يصبح Object).
  3. يُدرج تحويلات الأنواع (Casts) اللازمة في البايت كود.
  4. بما أن المترجم تحقق من كل شيء، فإن هذه التحويلات المُدرجة آمنة 100%.
  5. النتيجة: تحصل على مرونة Object مع أمان الأنواع المحددة!

لهذا السبب تُعتبر الأنماط المعممة (Generics) ميزة وقت الترجمة فقط (Compile-Time Feature). في وقت التشغيل (Runtime)، لا يوجد أثر للأنماط المعممة -- لا <Juice>، ولا <Water>، ولا <T>. فقط Object وتحويلات آمنة. الـ JVM لا يعرف أبدًا أن الأنماط المعممة كانت موجودة في الكود المصدري الخاص بك.

6 الإثبات — تفكيك البايت كود (Decompiling)

إذا لم تصدقني، دعنا نثبت ذلك! إذا أخذت ملف Glass.class المترجم وفككته (Decompile) إلى كود جافا المصدري مرة أخرى (باستخدام أدوات مثل javap أو أي مفكك جافا)، ستجد شيئًا مثيرًا للاهتمام جدًا...

الخطوة 1 ترجمة صنف الكأس (Glass Class)

Terminal
$ javac Glass.java $ ls Glass.class Glass.java

لاحظ: ملف .class واحد فقط، وليس ثلاثة! بغض النظر عن عدد الأنواع المختلفة التي تستخدمها مع Glass<T> -- Glass<Juice>، Glass<Water>، Glass<Honey> -- هناك دائمًا Glass.class واحد فقط.

الخطوة 2 التفكيك باستخدام javap

javap output
$ javap -c Glass.class Compiled from "Glass.java" public class Glass { public Glass(java.lang.Object); public java.lang.Object getLiquid(); public void setLiquid(java.lang.Object); }
الإثبات!
كل T تم استبداله بـ java.lang.Object. معلومات الأنواع المعممة اختفت تمامًا من البايت كود! لا يوجد ذكر لـ <T>، ولا ذكر لـ Juice أو Water -- فقط Object عادي. الـ JVM يرى صنفًا عاديًا تمامًا، غير معمم.

7 لماذا هذا مهم؟

الآثار العملية لمحو الأنواع (Type Erasure)
  1. يوجد ملف .class واحد فقط لكل صنف معمم، بغض النظر عن عدد وسائط الأنواع (Type Arguments) التي تستخدمها. Glass<Juice>، Glass<Water>، Glass<String> -- جميعها تتشارك نفس ملف Glass.class.
  2. الأنماط المعممة (Generics) لا تضيف أي عبء إضافي في وقت التشغيل (Runtime) -- لا أصناف إضافية، ولا ذاكرة إضافية. البايت كود مطابق لما كنت ستكتبه يدويًا باستخدام Object.
  3. لا يمكنك فحص الأنواع المعممة في وقت التشغيل -- على سبيل المثال، if (g instanceof Glass<Juice>) لن يعمل لأن <Juice> غير موجود في وقت التشغيل.
  4. لهذا السبب تُسمى الأنماط المعممة "آلية وقت الترجمة" (Compile-Time Mechanism) -- تؤدي عملها بالكامل قبل أن يعمل البرنامج أبدًا.

8 الملخص — الصورة الكاملة

دعنا نتراجع خطوة ونرى الصورة الكاملة لكل ما تعلمناه عن الأنماط المعممة (Generics) عبر الدروس الخمسة جميعها.

الأنماط المعممة (Generics) — القصة الكاملة
  • الأنماط المعممة (Generics) أداة تم تقديمها في JDK 1.5 للتخلص من التحويل القسري (Casting) ومنع ClassCastException.
  • تعمل في وقت الترجمة فقط -- المترجم يتحقق من الأنواع ثم يمحوها.
  • بعد المحو، T يصبح Object ويقوم المترجم بإدراج تحويلات آمنة تلقائيًا.
  • يمكنك جعل الأصناف (Classes) والتوابع (Methods) والواجهات (Interfaces) معممة -- جميعها تستخدم نفس آلية <T>.
  • النتيجة: تابع/صنف واحد لجميع الأنواع، بدون تحويل في كودك، وبدون أخطاء أنواع في وقت التشغيل.
ما التالي؟
في الدرس القادم، سنتعلم عن أنماط البدل المعممة (Wildcard Generics) (?) -- طريقة خاصة لجعل الأنماط المعممة أكثر مرونة. ترقبوا!

9 النقاط الرئيسية

ما تعلمته في هذا الدرس
  1. محو الأنواع (Type Erasure) يزيل جميع معلومات الأنواع المعممة بعد الترجمة -- الأنماط المعممة موجودة فقط في الكود المصدري، وليس أبدًا في البايت كود.
  2. يتم استبدال T بـ Object في البايت كود -- كل معامل نوع يصبح Object بعد أن ينتهي المترجم.
  3. المترجم يُدرج التحويلات تلقائيًا -- وهي مضمونة أن تكون آمنة لأن المترجم تحقق من الأنواع أولًا.
  4. يتم إنشاء ملف .class واحد فقط لكل صنف معمم -- بغض النظر عن عدد وسائط الأنواع المختلفة التي تستخدمها.
  5. الأنماط المعممة (Generics) موجودة في وقت الترجمة فقط -- لا يوجد أثر لها في وقت التشغيل (Runtime).
  6. الأنماط المعممة لا تضيف أي عبء إضافي في وقت التشغيل -- البايت كود بنفس كفاءة كود Object المكتوب يدويًا.
  7. المترجم هو المدقق الإملائي -- يتحقق من أن جميع الأنواع صحيحة قبل محوها، مما يضمن أمان الأنواع بشكل كامل.
← صفحة الوحدة الدرس 4: الواجهات المعممة →
سمة كتل الكود