Generics في جافا: السر وراء الكود الآمن والقابل لإعادة الاستخدام!

مرحباً بك في عالم Generics! تخيل أنك تريد صنع صندوق. بدون Generics، هذا الصندوق يمكنه حمل أي شيء: كتاب، تفاحة، قلم... وهذا قد يسبب فوضى! ماذا لو أردت صندوقاً مخصصاً لحمل الكتب فقط، وآخر لحمل الأقلام فقط؟ هنا يأتي دور Generics في جافا. ببساطة، Generics تسمح لك بإنشاء فئات، وواجهات، وطرق تعمل مع أنواع بيانات محددة، مما يجعل الكود أكثر أماناً وأسهل في القراءة وإعادة الاستخدام.


🎯 لماذا نحتاج Generics؟ مشكلة "النوع الخام" (Raw Type)

قبل ظهور Generics في Java 5، كان المبرمجون يستخدمون فئات مثل ArrayList بالطريقة التالية:

import java.util.ArrayList;

public class BeforeGenerics {
    public static void main(String[] args) {
        // قائمة "خام" يمكنها تخزين أي كائن
        ArrayList list = new ArrayList();
        list.add("كتاب جافا"); // إضافة نص
        list.add(100); // إضافة رقم! (سيتم تحويله تلقائياً إلى كائن Integer)

        // عند استرجاع العنصر، يجب تحويله يدوياً (Casting)
        String book = (String) list.get(0); // هذا يعمل
        String number = (String) list.get(1); // خطأ في وقت التشغيل! ClassCastException
    }
}

المشاكل هنا:

  1. عدم الأمان: يمكن إضافة أي نوع من البيانات، مما قد يؤدي إلى أخطاء غير متوقعة.
  2. الحاجة للتحويل اليدوي (Casting): يجب عليك دائماً تحويل العنصر المسترجع إلى النوع الصحيح.
  3. صعوبة اكتشاف الأخطاء: الخطأ (مثل محاولة تحويل Integer إلى String) لن يظهر إلا أثناء تشغيل البرنامج (Runtime)، وليس أثناء كتابته (Compile-time).

✨ الحل: Generics لضمان النوع (Type Safety)

مع Generics، نحدد النوع الذي نريده عند إنشاء الكائن. يشبه هذا إعطاء "وصفة" أو "قالب" للفئة.

import java.util.ArrayList;

public class WithGenerics {
    public static void main(String[] args) {
        // الآن نحدد أن هذه الـ ArrayList مخصصة *فقط* لنوع String
        ArrayList<String> bookList = new ArrayList<String>();
        bookList.add("كتاب جافا");
        bookList.add("كتاب الخوارزميات");
        // bookList.add(100); // خطأ فوري أثناء الكتابة! لن يسمح لك المترجم

        // لا حاجة للتحويل اليدوي (Casting) بعد الآن!
        String firstBook = bookList.get(0); // آمن ومضمون
        System.out.println("الكتاب الأول: " + firstBook);
    }
}

المزايا:

  1. الأمان: المترجم (Compiler) يكتشف الأخطاء المتعلقة بالنوع أثناء الكتابة.
  2. الوضوح: الكود يصبح أوضح، فأنت تعرف بالضبط نوع البيانات المخزنة.
  3. إلغاء الحاجة للتحويل اليدوي: الاسترجاع يكون مباشراً وآمناً.

🏗️ كيفية إنشاء فئة Generic خاصة بك

لنصنع صندوقنا الآمن الخاص! نستخدم رموزاً مثل <T> أو <E> داخل علامات الأقواس المثلثة <> لتمثيل "معامل النوع" (Type Parameter).

// T هي معلمة نوع (Type Parameter). يمكنك استخدام أي حرف، لكن T شائع لتمثيل "Type"
public class SafeBox<T> {
    private T item; // سوف يحتفظ بصندوقنا بعنصر من النوع T

    // طريقة لوضع عنصر في الصندوق
    public void put(T item) {
        this.item = item;
    }

    // طريقة لأخذ العنصر من الصندوق
    public T get() {
        return this.item;
    }

    public static void main(String[] args) {
        // صندوق مخصص للكتب (String)
        SafeBox<String> bookBox = new SafeBox<>();
        bookBox.put("الرواية المفضلة");
        String myBook = bookBox.get(); // آمن: الناتج هو String

        // صندوق مخصص للأرقام (Integer)
        SafeBox<Integer> numberBox = new SafeBox<>();
        numberBox.put(42);
        int myNumber = numberBox.get(); // آمن: الناتج هو Integer
    }
}

شرح <T>: تخيل أن T هي "مكان فارغ" لاسم نوع. عندما تنشئ كائن SafeBox<String>، يستبدل المترجم كل T في الفئة بـ String. وعندما تنشئ SafeBox<Integer>، يستبدل كل T بـ Integer.


📝 استخدام أكثر من معامل نوع (Multiple Type Parameters)

يمكنك استخدام أكثر من حرف، مثل <K, V> شائع في الهاش ماب (المفتاح، القيمة).

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }

    public static void main(String[] args) {
        // زوج من النصوص
        Pair<String, String> namePair = new Pair<>("الاسم الأول", "أحمد");
        
        // زوج من نص ورقم
        Pair<String, Integer> agePair = new Pair<>("العمر", 25);
        System.out.println(agePair.getKey() + ": " + agePair.getValue());
    }
}

🔧 الطرق الـ Generic (Generic Methods)

يمكنك أيضاً تعريف طرق تستخدم Generics، حتى لو كانت داخل فئة عادية (غير Generic).

public class Utility {
    // طريقة عامة تأخذ مصفوفة من أي نوع T وتطبع محتوياتها
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        String[] names = {"أحمد", "محمد", "فاطمة"};
        Integer[] numbers = {1, 2, 3, 4, 5};

        Utility.printArray(names);   // تعمل مع المصفوفة النصية
        Utility.printArray(numbers); // تعمل مع المصفوفة الرقمية
    }
}

لاحظ كيف أن تعريف النوع العام <T> يأتي قبل نوع الإرجاع للدالة (void في هذه الحالة).


📚 Generics مع الوراثة و Wildcards (?)

أحياناً نريد كتابة كود مرن يعمل مع عائلة من الأنواع. هنا نستخدم الرمز البدل ? (Wildcard).

import java.util.List;

public class WildcardExample {
    // طريقة تقبل قائمة من أي نوع (مجهول)
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }

    // طريقة تقبل قائمة من أي نوع يكون Number أو فرع منه (مثل Integer, Double)
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    // طريقة تقبل قائمة من أي نوع يكون Integer أو أي أصل له (مثل Object)
    // public static void addInteger(List<? super Integer> list) { ... }
}

🎓 ملخص الدرس

  • Generics تضيف أمان النوع (Type Safety) للكود عن طريق اكتشاف الأخطاء أثناء الترجمة.
  • تزيل الحاجة للتحويل اليدوي (Casting) غير الآمن.
  • تجعل الكود أكثر وضوحاً وقابلاً لإعادة الاستخدام بشكل كبير.
  • نستخدم معاملات النوع <T> لإنشاء فئات وطرق عامة.
  • نستخدم الرمز البدل ? للتعامل مع مجموعات من الأنواع المرتبطة بالوراثة.