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

الأصناف المعممة (Generic Classes) — مثال الكأس

تصميم صنف واحد يعمل بأمان مع أي نوع

Glass<T> معامل النوع (Type Parameter) بدون تحويل قسري (No Casting) صنف واحد (One Class)

1 تذكّر المشكلة

في الدرس الأول، رأينا أن استخدام Object في البرمجة المعممة يتطلب تحويلًا قسريًا (Casting)، وأن التحويل القسري قد يتسبب في ClassCastException أثناء التشغيل. لم يستطع المترجم مساعدتنا — فهو لم يرَ أي خطأ. ظهر الخطأ فقط عند تشغيل البرنامج، وعندها كان الأوان قد فات.

الآن دعونا نحل هذه المشكلة باستخدام الأنماط المعممة (Generics). نريد من المترجم أن يكون بمثابة المدقق الإملائي — يكتشف أخطاء الأنواع قبل تشغيل البرنامج.

2 تعريف صنف معمم (Generic Class)

الفكرة بسيطة. بعد اسم الصنف، نضيف أقواس زاوية (Angle Brackets) مع معامل نوع (Type Parameter) بداخلها:

public class Glass<T> { // T is a placeholder for any type }

T هو عنصر نائب (Placeholder) — يمثل أي نوع. يمكن أن يكون Juice أو Water أو Honey أو أي شيء آخر. لا نقرر ما هو T عندما نكتب الصنف. بل نقرر ذلك عندما ننشئ كائنًا من هذا الصنف.

فكّر فيه مثل سطر فارغ في استمارة. تقول الاستمارة “الاسم: ______”. الفراغ هو عنصر نائب. تملأه لاحقًا عندما تستخدم الاستمارة فعليًا.

اصطلاح تسمية معاملات النوع (Type Parameters)

وفقًا للاصطلاح، تستخدم Java حروفًا كبيرة مفردة لمعاملات النوع. يمكنك استخدام أي اسم (مثل ABC أو MyType)، لكن الاصطلاح هو استخدام حروف مفردة. هذا يوضح فورًا أنك تنظر إلى معامل نوع وليس اسم صنف عادي.

اصطلاحات معاملات النوع القياسية

T — النوع (Type) (الأكثر شيوعًا، معامل عام الاستخدام)

E — العنصر (Element) (يُستخدم في مجموعات Java، مثل ArrayList<E>)

K — المفتاح (Key) (يُستخدم في الخرائط، مثل Map<K, V>)

V — القيمة (Value) (يُستخدم في الخرائط، مثل Map<K, V>)

N — العدد (Number)

S, U — معاملات النوع الثاني والثالث عندما يكون T مستخدمًا بالفعل

3 صنف الكأس — النسخة المعممة

لنكتب الآن صنف Glass الكامل باستخدام الأنماط المعممة. قارن ذهنيًا هذا مع النسخة المبنية على Object من الدرس الأول — لاحظ كيف تم استبدال كل Object بـ T.

Glass.java
public class Glass<T> { private T liquid; public Glass() { } public Glass(T liquid) { this.liquid = liquid; } public T getLiquid() { return liquid; } public void setLiquid(T liquid) { this.liquid = liquid; } @Override public String toString() { return "Glass contains: " + liquid; } }
ملاحظة مهمة

لاحظ: في كل مكان استخدمنا فيه Object سابقًا، نستخدم الآن T. نوع الحقل هو T. التابع getter يُرجع T. التابع setter يقبل T. النوع لم يعد ثابتًا — سيُحدَّد عند إنشاء كائن من هذا الصنف.

4 استخدام الصنف المعمم

الآن يأتي الجزء الجميل. عندما ننشئ كائنًا من Glass، نحدد النوع الذي يجب أن يكون عليه T. نضع النوع الفعلي داخل أقواس زاوية. شاهد — لا يوجد تحويل قسري في أي مكان!

TestGlass.java
// Create a glass parameterized for Juice Glass<Juice> juiceGlass = new Glass<>(); juiceGlass.setLiquid(new Juice()); Juice j = juiceGlass.getLiquid(); // No casting needed! // Create a glass parameterized for Water Glass<Water> waterGlass = new Glass<>(); waterGlass.setLiquid(new Water()); Water w = waterGlass.getLiquid(); // No casting needed! // Create a glass parameterized for Honey Glass<Honey> honeyGlass = new Glass<>(); honeyGlass.setLiquid(new Honey()); Honey h = honeyGlass.getLiquid(); // No casting needed!
لا تحويل قسري في أي مكان!

المترجم يعرف بالضبط ما النوع الموجود في كل كأس. juiceGlass يحتوي على Juice، و waterGlass يحتوي على Water، و honeyGlass يحتوي على Honey. عند استدعاء getLiquid()، يُرجع النوع الذي حددته بالضبط — بدون تحويل قسري، بدون تخمين، بدون مخاطر.

5 أمان وقت الترجمة (Compile-Time Safety) — مقارنة جنبًا إلى جنب

دعونا نضع الطريقة القديمة والطريقة الجديدة جنبًا إلى جنب حتى تتمكن من رؤية ما تغير بالضبط.

قبل — باستخدام Object
Glass g = new Glass(); g.setLiquid(new Juice()); // Must cast — risky! Juice j = (Juice) g.getLiquid();
بعد — باستخدام الأنماط المعممة (Generics)
Glass<Juice> g = new Glass<>(); g.setLiquid(new Juice()); // No cast needed — safe! Juice j = g.getLiquid();

أمان ماذا لو حاولت استخدام النوع الخاطئ؟

هنا تتألق الأنماط المعممة حقًا. ماذا يحدث إذا حاول مطور عن طريق الخطأ وضع Water في كأس تم التصريح عنه لـ Juice؟

Compile Error Example
Glass<Juice> juiceGlass = new Glass<>(); juiceGlass.setLiquid(new Water()); // COMPILE ERROR! // Error: incompatible types: Water cannot be converted to Juice
مخرجات المترجم (Compiler Output)
$ javac TestGlass.java TestGlass.java:3: error: incompatible types: Water cannot be converted to Juice juiceGlass.setLiquid(new Water()); ^ 1 error
تم اكتشافه وقت الترجمة!

تم اكتشاف الخطأ في وقت الترجمة (Compile Time)، وليس وقت التشغيل! المدقق يعمل. لا يمكنك حتى تشغيل البرنامج حتى تصلح هذا الخطأ. هذا بالضبط ما أردناه.

مقارنة مع طريقة Object

مع صنف Glass القديم المبني على Object، كان نفس الخطأ سيُترجم بنجاح تام. لم يكن المترجم ليرى أي خطأ. ثم أثناء التشغيل — انهيار! يظهر ClassCastException من العدم. مع الأنماط المعممة، يتم القضاء على هذه الفئة الكاملة من الأخطاء.

6 ملف صنف واحد (One .class File) — سؤال طالب

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

سؤال ممتاز! لا — تحصل فقط على ملف واحد: Glass.class. ليس ثلاثة. واحد فقط.

قد يبدو هذا مفاجئًا. إذا كان هناك ملف صنف واحد فقط، فكيف تحافظ Java على فصل الأنواع؟ هذا سؤال متابعة رائع، وسنجيب عليه في الدرس الخامس عندما ندرس محو النوع (Type Erasure).

ملف .class واحد لجميع معاملات النوع

بغض النظر عن عدد معاملات النوع المختلفة التي تستخدمها — Glass<Juice> أو Glass<Water> أو Glass<String> أو Glass<Integer> — ينتج المترجم ملف Glass.class واحدًا فقط. يتم فحص الأنواع في وقت الترجمة، ثم تُزال معلومات النوع. سنتعلم بالضبط كيف يعمل هذا في الدرس الخامس (محو النوع - Type Erasure).

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

ما يجب أن تتذكره من هذا الدرس
  1. أضف <T> بعد اسم الصنف لجعله معممًا.
  2. استخدم T في كل مكان كنت ستستخدم فيه Object.
  3. عند إنشاء كائن، حدد النوع الفعلي: new Glass<Juice>()
  4. لا حاجة للتحويل القسري — المترجم يفرض أمان الأنواع.
  5. الأنواع الخاطئة تُكتشف في وقت الترجمة (Compile Time)، وليس وقت التشغيل.
  6. يُنتَج ملف .class واحد فقط بغض النظر عن معاملات النوع.
  7. الاصطلاح: T (النوع)، E (العنصر)، K (المفتاح)، V (القيمة).
ما التالي؟

لقد أنشأنا صنفًا معممًا. لكن ماذا لو لم تكن بحاجة إلى صنف كامل — ماذا لو كنت تحتاج فقط إلى تابع واحد يعمل مع أي نوع؟ في الدرس التالي، سنتعلم عن التوابع المعممة (Generic Methods).

الدرس 3: التوابع المعممة ← → الدرس 1: المشكلة
سمة كتل الكود