Menu
flag Ar iconالعربيةdown icon

الأنواع العامة في جافا: أصناف وطرق آمنة من حيث النوع

ما هي الأنواع العامة (Generics) في جافا، وكيفية كتابة أصناف وطرق عامة، والمعاملات النوعية المقيَّدة، والأحرف البدلية، ولماذا يهمّ محو النوع.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

لماذا توجد الأنواع العامة

يتيح لك النوع العام كتابة الشيفرة مرة واحدة وإعادة استخدامها مع أنواع كثيرة دون التخلّي عن أمان الأنواع. فبدلًا من كتابة IntBox وStringBox وUserBox بشكل منفصل، تكتب صنفًا واحدًا Box<T> حيث يكون T موضعًا نائبًا يملؤه الطرف المُستدعي.

لقد استخدمت الأنواع العامة بالفعل في كل مرة كتبت فيها ArrayList<String> أو HashMap<String, Integer>. الجزء <...> هو وسيط نوعي. توضّح هذه الصفحة كيف تكتب أنواعك العامة الخاصة بك.

أمّا البديل - تخزين كل شيء على أنه Object - فيُلقي بالمعلومات النوعية بعيدًا ويفرض تحويلات قبيحة ومعرّضة للأخطاء:

ذلك التحويل في السطر الأخير يطلق ClassCastException وقت التشغيل - وهو نوع الأخطاء الذي صُمّمت الأنواع العامة لجعله مستحيلًا.

صنف عام

أعلن عن معامل نوعي بين قوسين زاويين بعد اسم الصنف. وبحسب العُرف يكون حرفًا كبيرًا واحدًا: T لـ "type" (نوع)، وE لـ "element" (عنصر)، وK/V للمفتاح/القيمة.

داخل Box، يصبح كل T ما زوّده به الطرف المُستدعي. وBox<String> صندوق يحتوي ويُعيد String فقط. ويرفض المترجِم name.set(99) قبل أن يعمل البرنامج أصلًا.

أمّا <> الفارغة على الطرف المقابل (مُعامِل الماسة) فتدع المترجِم يستنتج الوسيط النوعي من الجهة المقابلة، فلا تكرّر <String> مرتين.

الطرق العامة

يمكن أن يكون لطريقة واحدة معاملها النوعي الخاص بها، مستقلًّا عن الصنف. ضع المعامل <T> قبل نوع القيمة المُعادة:

أنت لا تمرّر T صراحةً أبدًا - بل يستنتجه المترجِم من الوسيط. والطرق العامة هي السبيل الذي يبقي به أدواتٌ مثل Collections.sort أو List.of آمنةً من حيث النوع مع أي نوع عنصر.

المعاملات النوعية المقيَّدة

أحيانًا لا يكون للنوع العام معنى إلا مع بعض الأنواع. تقيّد extends المعامل حتى تتمكّن من استدعاء طرق الحدّ. هنا يعني T extends Number أن T هو Number أو أي صنف فرعي منه (Integer، Double، ...)، فتُصبح doubleValue() متاحة:

لاحظ أن extends هنا تعني "نوع فرعي من"، وهي تعمل مع الأصناف والواجهات على حدٍّ سواء - والصيغة <T extends Comparable<T>> شائعة جدًّا حين تحتاج إلى مقارنة العناصر.

الأحرف البدلية: ‎? extends‎ و‎? super‎

مزلق دقيق: List<Integer> ليست List<Number>، رغم أن Integer هو Number. فالأنواع العامة ثابتة (invariant). والأحرف البدلية تخفّف هذا القيد حين تحتاج إلى القراءة فقط أو الكتابة فقط.

استخدم ? extends T لمُنتِجٍ تقرأ منه، و? super T لمستهلِكٍ تكتب إليه (قاعدة "PECS" - Producer Extends, Consumer Super):

تتيح لك قائمة ? extends Number قراءة العناصر بوصفها Number، لكنها لا تتيح الإضافة إليها (إذ لا يستطيع المترجِم معرفة نوع العنصر الدقيق). وتتيح لك قائمة ? super Integer إضافة Integer، لكن القراءات تعود بوصفها Object. اختر الحرف البدلي الذي يناسب طريقة تدفّق البيانات.

محو النوع وحدوده

الأنواع العامة ميزة خاصة بوقت الترجمة. فبعد الترجمة يُمحى المعامل النوعي - ووقت التشغيل تكون Box<String> وBox<Integer> كلتاهما مجرد Box. يُبقي هذا الأنواع العامة متوافقة رجعيًّا مع الشيفرة القديمة، لكنه يفرض حدودًا حقيقية.

// لا يُترجَم أيٌّ مما يلي - المعامل النوعي غير موجود وقت التشغيل:
T value = new T();          // لا يمكن إنشاء كائن من معامل نوعي
T[] array = new T[10];      // لا يمكن إنشاء مصفوفة عامة
if (list instanceof List<String>) { } // لا يمكن اختبار الوسيط النوعي

ولأن النوع يختفي وقت التشغيل، لا يمكنك السؤال "ماذا كان T؟" عبر الانعكاس، ولا يمكنك تحميل طرق زائد (overload) تختلف فقط في وسيطها العام (إذ تُمحى foo(List<String>) وfoo(List<Integer>) إلى البصمة نفسها). وحين تحتاج فعلًا إلى النوع وقت التشغيل، مرّر رمز Class<T> بوصفه معاملًا للباني أو للطريقة.

التالي: تعبيرات لامدا

رأيت أن الطريقة العامة تأخذ نوعًا بوصفه معاملًا. والخطوة التالية هي معاملة السلوك بوصفه معاملًا. تتيح لك تعبيرات لامدا تمرير جزء من الشيفرة - دالة - إلى طريقة، وهذا بالضبط ما ستفرز به وتُرشّح وتُحوّل المجموعات العامة التي تعلّمت لتوّك كتابتها بأمان من حيث النوع.

الأسئلة الشائعة

ما هي الأنواع العامة في جافا؟

تتيح لك الأنواع العامة كتابة صنف أو طريقة تعمل مع نوع تحدّده لاحقًا، بدلًا من حصرها في نوع محدّد واحد. تُعلِن عن معامل نوعي بين قوسين زاويين - class Box<T> - ويملأ الطرف المُستدعي النوع الفعلي - Box<String>. عندئذٍ يفرض المترجِم هذا النوع في كل مكان، فتلتقط حالات عدم التطابق وقت الترجمة وتستغني عن التحويلات اليدوية.

لماذا نستخدم الأنواع العامة بدلًا من Object؟

استخدام Object يفقد كل المعلومات النوعية: لا يستطيع المترجِم منعك من وضع الشيء الخطأ، وعليك تحويل كل قيمة تخرج (مع خطر ClassCastException وقت التشغيل). تنقل الأنواع العامة هذا الفحص إلى وقت الترجمة. فقائمة List<String> لن تقبل Integer ببساطة، وget() تُعيد String بالفعل - بلا تحويل وبلا مفاجآت وقت التشغيل.

ما هو محو النوع (type erasure) في الأنواع العامة بجافا؟

يعني محو النوع أن معلومات النوع العام موجودة وقت الترجمة فقط. بعد الترجمة، تكون List<String> وList<Integer> كلتاهما مجرد List وقت التشغيل - إذ يُمحى المعامل النوعي. لهذا لا يمكنك كتابة new T[10]، ولا استدعاء list instanceof List<String>، ولا قراءة معامل نوعي عبر الانعكاس (reflection). تمنحك الأنواع العامة أمانًا وقت الترجمة، لا بياناتٍ نوعية وقت التشغيل.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن