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

لماذا الجينيريكس (المعمّمات)؟

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

لماذا الجينيريكس (المعمّمات)؟

أُضيفت الجينيريكس إلى Java 5 لحلّ مشكلة كانت تُعاني منها كل قاعدة كود تعتمد على المجموعات (Collections): الغياب التام لأمان النوع في وقت الترجمة. فهم سبب وجودها هو أسرع طريق لاستخدامها بشكل صحيح.

الحياة قبل الجينيريكس: عصر المجموعات الخام

قبل Java 5، كانت ArrayList (وكل مجموعة أخرى) تخزّن مراجع من نوع Object الخام. كنت تضع فيها أي شيء، وتحصل على Object عند الاسترجاع — مما يعني أنّك كنت مضطرًا لتحويل (cast) كل ما تسترجعه.

// أسلوب Java 1.4 — لا يزال قانونيًا اليوم لكنه مُحبَّط بشدة import java.util.ArrayList; public class PreGenerics { public static void main(String[] args) { ArrayList names = new ArrayList(); // نوع خام names.add("Alice"); names.add("Bob"); names.add(42); // لا خطأ هنا! for (Object o : names) { String name = (String) o; // ClassCastException في وقت التشغيل حين o == 42 System.out.println(name.toUpperCase()); } } }

قبل الجينيريكس، كان المترجم يقبل هذا الكود دون أي تحذير. كان الخطأ يعيش بصمت حتى يواجهه المستخدم على شكل ClassCastException في وقت التشغيل.

ClassCastException قنبلة موقوتة في وقت التشغيل. كل تحويل نوع تكتبه على مجموعة خام هو وعد تقطعه للمترجم، ولا يمكنك الوفاء به إذا كانت البيانات لا تتطابق. المترجم لا يستطيع التحقق من هذا الوعد — الـ JVM وحده يفعل ذلك، وذلك برمي استثناء.

أوجاع الثلاثة في عصر النوع الخام

  1. تحويلات النوع غير الآمنة في كل مكان. كل استدعاء get() كان يحتاج إلى تحويل. انسَ واحدًا أو حوّل إلى نوع خاطئ، وستحصل على تعطّل في وقت التشغيل.
  2. لا توثيق في النوع ذاته. معامل مكتوب كـ List لا يخبرك بشيء. هل هي قائمة من String؟ أم Integer؟ أم مختلطة؟ كنت مضطرًا لقراءة Javadoc أو الكود المصدري لتعرف.
  3. لا مساعدة من الأدوات. لم تستطع بيئات التطوير اقتراح توابع على نوع العنصر لأنّ النوع كان ممحوًا إلى Object. كان الإكمال التلقائي عديم الفائدة لمحتوى المجموعات.

الجينيريكس تحلّ المشاكل الثلاث

النوع المعمّم يحمل نوع عنصره كـ معامل نوع. تكتب ArrayList<String> بدلًا من ArrayList. يعرف المترجم الآن أن كل عنصر من نوع String ويُطبّق ذلك في وقت الترجمة.

import java.util.ArrayList; import java.util.List; public class WithGenerics { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); // names.add(42); // خطأ في الترجمة — أنواع غير متوافقة: int لا يمكن تحويله إلى String for (String name : names) { // لا حاجة لتحويل النوع — المترجم يضمن النوع System.out.println(name.toUpperCase()); } } }

ثلاثة مكاسب في تغيير واحد:

  • رفض add(42) الخاطئة في وقت الترجمة، لا في وقت التشغيل.
  • متغيّر الحلقة مكتوب كـ String بالفعل — لا تحويل، لا ضجيج.
  • بيئة التطوير تقدّم الآن الإكمال التلقائي الكامل لـ String داخل الحلقة.
اكشف الأخطاء مبكرًا. خطأ وقت الترجمة أرخص بلا حدود من استثناء وقت التشغيل. المستخدم لا يراه، CI يكشفه في ثوانٍ، ولا يوجد stack trace لتفسيره. تنقل الجينيريكس أخطاء النوع إلى أقرب نقطة ممكنة في دورة التطوير.

الجينيريكس تتجاوز المجموعات

المجموعات هي حالة الاستخدام الأكثر وضوحًا، لكن الجينيريكس ميزة عامة في اللغة. أي فئة أو تابع يعمل على نوع لا يعرفه مسبقًا يمكن تعميمه. مثال كلاسيكي: Optional<T>.

import java.util.Optional; public class OptionalDemo { public static void main(String[] args) { Optional<String> maybeUser = findUser(1); // orElse يُعيد String — لا تحويل، آمن تمامًا على مستوى النوع String user = maybeUser.orElse("Guest"); System.out.println(user.toUpperCase()); } static Optional<String> findUser(int id) { return id == 1 ? Optional.of("Alice") : Optional.empty(); } }

Optional<String> تقول: "هذا الصندوق إمّا يحتوي على String أو فارغ." يُطبّق المترجم هذا الضمان. بدون الجينيريكس، كان Optional سيُعيد Object وستعود إلى تحويل الأنواع.

نظرة سريعة على نمط الصيغة

الصيغة بين الزوايا <T> تُعلن عن معامل نوع. عند استخدام النوع المعمّم، تُزوّد وسيطة نوع مثل String أو Integer:

// الإعلان — T هو معامل النوع (عنصر نائب) class Box<T> { private T value; Box(T value) { this.value = value; } T get() { return value; } } // الاستخدام — String هي وسيطة النوع Box<String> strBox = new Box<>("hello"); String s = strBox.get(); // يُعيد String، لا تحويل Box<Integer> intBox = new Box<>(42); int n = intBox.get(); // يُعيد Integer (يُفرَّغ تلقائيًا)، لا تحويل

نفس فئة Box تعمل مع أي نوع، ومع ذلك يكشف المترجم عدم التطابق في النوع لكل استخدام محدد. هذا هو الوعد الجوهري للجينيريكس: اكتب مرة واحدة، وتحقّق النوع لكل استخدام.

استخدم مشغّل الماسة <> على الجانب الأيمن (كما في الأمثلة أعلاه) بدلًا من تكرار وسيطة النوع الكاملة. المترجم يستنتجها من الجانب الأيسر. تكرارها زائد ويُثقّل الكود.

الخلاصة

وُجدت الجينيريكس لأن المجموعات الخام كانت تُجبر المطوّرين على تحويل كل شيء، وتؤجّل أخطاء النوع إلى وقت التشغيل. بتعميم الأنواع بـ <T>، يستطيع المترجم التحقق من أن النوع الصحيح فقط يُخزَّن ويُسترجَع. النتيجة: كود أكثر أمانًا، وتوثيق أفضل من خلال الأنواع، وتجربة أفضل بكثير في بيئة التطوير — كل ذلك مجانًا في وقت التشغيل بسبب محو النوع (يُغطّى في الدرس 7). في الدروس التالية ستتعلم كتابة فئاتك وتوابعك المعمّمة الخاصة، وتقييد معاملات النوع بالحدود.