مفهوم الـ generics في جافا | Java generics

مفهوم الـ generics في جافا

مقدمة للـ Generics في جافا

في الدرسين السابقين تعرفنا على كلاسات الـ Data Structure التي تسمح لنا بتخزين البيانات مهما كان نوعهم و بأشكال مختلفة أثناء تشغيل البرنامج.
عند استخدام هذه الكلاسات كنا نلاحظ أنه بإمكاننا تخزين أي نوع بيانات فيهم بدون أي مشاكل.

أسلوب الـ Generics يجعلك قادراً على بناء كود واحد يلائم أكثر من نوع بيانات, و عند الحاجة إلى استخدام هذا الكود يمكنك إستخدامه كما هو, و يمكنك أيضاً تحديد نوع البيانات التي تريده أن يعمل معها حسب حاجتك.

في الدرس السابق شرحنا كيف يمكن تخزين أي نوع بيانات في الكلاسات ArrayList و LinkedList, و تعلمنا أيضاً كيف أننا نستطيع تحديد نوع البيانات التي نريد تخزينها فيهم في حال استخدمنا أسلوب الـ Generics عند إنشاء كائنات منهم.


معلومة تقنية

فعلياً, معظم كلاسات الـ Data Structure تم بناءها على أساس هذا الأسلوب.
كل كلاس, إنترفيس أو دالة تجد الرمز < > موجود ضمن تعريفه, إعرف مباشرةً أنه مصمم للتعامل مع أكثر من نوع بيانات.


فائدة الـ Generics

أنت لست مجبر على استخدام أسلوب الـ Generics, لكن تعلمه سيفيدك كثيراً في تطوير الكود الخاص فيك, و جعل حجمه أصغر و أسهل في حالة التعديل.
فمثلاً, إن كنت بحاجة إلى بناء مصفوفة ليس لها حجم محدد و تريد تخزين أعداد صحيحة ( أي من النوع int ) فقط فيها, يمكنك إنشاء كائن من الكلاس ArrayList و تحديد النوع Integer كنوع البيانات الوحيد الذي يمكن إدخاله في هذا الكائن كما رأينا في الدرس السابق و تكون بهذا قد وصلت لهدفك بكل سهولة بواسطة أسلوب الـ Generics.

ميزة أخرى مهمة جدًا في هذا الأسلوب, و هي أنه يساعد المبرمج في إكتشاف الأخطاء النوعية أثناء كتابة الكود.

مثلاً, إذا قمت بإنشاء كائن نوعه ArrayList و حددت أنه يمكنه تخزين قيم من النوع Integer فقط, بعدها حاولت تخزين قيمة من نوع آخر مثل النوع String على سبيل المثال, عندها سيظهر لك تحذير مفاده أنه لا يمكن تحويل النوع String إلى النوع Integer.


خلاصة

بشكل عام, نستخدم الـ Generics لبناء كود يتوافق مع أي نوع بيانات.
عند الحاجة إلى هذا الكود نقوم بتحديد نوع البيانات التي سنتعامل معها. ستفهم كل شيء من الأمثلة.


ستتعلم في هذا الدرس كيف تعرف أي شيء كـ Generic و كيف تستفيد منه, و تذكر أنه يمكنك استخدام هذا الأسلوب مع أغلب كلاسات الـ Data Structure.

الأحرف المستخدمة في الـ Generics في جافا

ذكرنا في الجدول التالي الأحرف المتعارف عليها بين المبرمجين عند التعامل مع الـ Generics.

الأحرف المتعارف عليها في الـ Generics
T إختصار للكلمة Type, يقصد منه أي نوع كان.
نستخدمه في العادة عند بناء كود يتعامل مع أي نوع بيانات.
N إختصار للكلمة Number, يقصد منه أي نوع من الأنواع التي تستخدم لتخزين الأرقام.
نستخدمه في العادة عند بناء كود يتعامل مع الأرقام من أي نوع كانت مثل ( Integer - Float - Double إلخ.. ).
E إختصار للكلمة Element, يقصد منه عنصر ليس له نوع محدد.
نستخدمه في العادة عند بناء كود يتعامل مع عناصر المصفوفة من أي نوع كانت.
K إختصار للكلمة Key, يقصد منه مفتاح ليس له نوع محدد.
نستخدمه في العادة عند بناء كود يتعامل مع مفاتيح كائن يخزن البيانات بشكل key / value.
V إختصار للكلمة Value, يقصد منه قيمة ليس لها نوع محدد.
نستخدمه في العادة عند بناء كود يتعامل مع قيم كائن يخزن البيانات بشكل key / value.

ملاحظة: جميع الأحرف المذكورة في الجدول ليس لها أي تأثير على الكود و يمكنك وضع أي حرف أو كلمة مكانهم, لكننا ننصحك باعتمادهم.

المبادئ الأساسية التي عليك إتباعها عند التعامل مع الـ Generics في جافا

لتعريف متغير, مصفوفة أو دالة بدون تحديد نوعهم, أي ليعملوا مع مختلف أنواع البيانات, يجب تعريفهم كـ Generics.
هنا وضعنا أمثلة صغيرة الهدف منها توضيح طريقة استخدام الأحرف التي ذكرناها في الجدول السابق من أجل تعريف كود يمكن استخدامه للتعامل مع أكثر من نوع بيانات.

في نهاية الدرس وضعنا أمثلة مهمة توضح أهمية و طريقة استخدام الـ Generics.


طريقة تعريف نوع بيانات مجهول

لتجهيز نوع بيانات غير محدد يمكن استخدامه في كلاس أو إنترفيس أو دالة, نضع أحد الأحرف التي ذكرناها في الجدول السابق بين &lt > عند تعريفهم.

أمثلة

هنا قمنا بتعريف كلاس إسمه GenericClass يملك نوع بيانات غير محدد رمزنا له بـ T.

                    public class GenericClass <T> {

                    }
                  

هنا قمنا بتعريف إنترفيس إسمه GenericInterface يملك نوع بيانات غير محدد رمزنا له بحرف N.

                    public interface GenericInterface <N> {

                    }
                  

هنا قمنا بتعريف دالة إسمها genericMethod تملك نوع بيانات غير محدد رمزنا له بحرف E.

                    public <E> void genericMethod(E var) {
                    System.out.println(var);
                    } 
                  


طريقة تعريف أكثر من نوع بيانات مجهول

نفس الطريقة السابقة, لكن يجب وضع فاصلة بين كل حرفين تم استخدامها بداخل الرمز &lt >

أمثلة

هنا قمنا بتعريف كلاس إسمه GenericClass يملك نوعي بيانات غير محددين يرمز لهما بالحرفين E1 و E2.

                    public class GenericClass <E1, E2> {

                    }
                  

هنا قمنا بتعريف إنترفيس إسمه GenericInterface يملك نوعي بيانات غير محددين يرمز لهما بالحرفين E1 و E2.

                    public interface GenericInterface <E1, E2> {

                    }
                  

هنا قمنا بتعريف دالة إسمها genericMethod تملك نوعي بيانات غير محددين يرمز لهما بالحرفين T1 و T2.

                    public <T1, T2> void genericMethod(T1 a, T2 b) {
                    System.out.println(a);
                    System.out.println(b);
                    } 
                  

أمثلة شاملة حول التعامل مع الـ Generics في جافا

التعامل مع الـ Generics يكون بأشكال مختلفة ستتعرف عليها من الأمثلة التي وضعناها لك, كما أننا وضعنا خلاصة في نهاية كل مثال.


مصطلحات تقينة
  • Generic Method: تعني تعريف دالة تتعامل مع أكثر من نوع بيانات.

  • Generic Class: تعني تعريف كلاس يتعامل مع أكثر من نوع بيانات.

  • Bounded Type Parameters: تعني تعريف كلاس, إنترفيس أو دالة يتعاملوا مع نوع محدد. بالإضافة إلى جميع الأنواع المشتقة منه, أي التي ترث منه.



المثال الأول كيف تستغل مبدأ الـ Generics لبناء دالة تتعامل مع مختلف أنواع المصفوفات.

 هذا المثال  هو تطبيق لفكرة الـ Generic Method.
ستتعلم منه كيف تستغل مبدأ الـ Generics لبناء دالة تتعامل مع مختلف أنواع المصفوفات.

شاهد المثال »



المثال الثاني كيف تستغل مبدأ الـ Generics لبناء كلاس يتعامل مع مختلف الأنواع.

هذا المثال  هو تطبيق لفكرة الـ Generic Class.
ستتعلم منه كيف تستغل مبدأ الـ Generics لبناء كلاس يتعامل مع مختلف الأنواع.

شاهد المثال »



المثال الثالث كيف تستغل مبدأ الـ Generics لبناء دوال تتعامل مع مختلف أنواع الأرقام فقط.

هذا المثال  هو تطبيق لفكرة الـ Bounded Type Parameters.
ستتعلم في منه كيف تستغل مبدأ الـ Generics لبناء دوال تتعامل مع مختلف أنواع الأرقام فقط.

شاهد المثال »

&&&////////////////////&&&/////////////////&&&
&&&////////////////////&&&/////////////////&&&

 مثال حول تعريف و إستدعاء Generic Method في جافا

في المثال التالي قمنا ببناء دالة إسمها printArray, نوعها public static void.
هذه الدالة مصممة بشكل عام للتعامل مع مصفوفة ليس لها نوع محدد و هذا سبب وضع الحرف <E> بين الكلمتين static و void.

عند استدعاء هذه الدالة نمرر لها مصفوفة من أي نوع فتقوم بعرض محتواها.
إذاً E[] تعني أنه عند استدعاء هذه الدالة يجب تمرير مصفوفة من أي نوع كان فيها كـ Argument مكان الباراميتر array.

في كل مرة تقوم فيها باستدعاء الدالة printArray() يتم تبديل الحرف E بنوع المصفوفة التي تم إدخالها فيها مكان الباراميتر array.


Main.java
                    public class Main {

                    // argument عند استدعاءها نمرر لها مصفوفة من أي نوع كـ .void هنا قمنا بتعريف دالة نوعها
                    public static <E> void printArray( E[] array )
                    {
                    // التي ندخلها في الدالة عند استدعاءها array تعرض جميع عناصر المصفوفة foreach هنا قمنا بإنشاء حلقة
                    for( E element : array )
                    System.out.print(element + " ");

                    System.out.println();

                    }


                    public static void main(String[] args) {

                    // هنا قمنا بتعريف 6 مصفوفات, كل واحدة فيهم تملك 5 عناصر من نوع مختلف
                    Integer   arr1[] = {1, 2, 3, 4, 5};
                    Long      arr2[] = {1l, 2l, 3l, 4l, 5l};
                    Float     arr3[] = {1f, 2f, 3f, 4f, 5f};
                    Double    arr4[] = {1d, 2d, 3d, 4d, 5d};
                    Character arr5[] = {'a', 'b', 'c', 'd', 'e'};
                    String    arr6[] = {"Generics ", "are ", "easy ", "to ", "understand"};

                    // لعرض محتوى جميع المصفوفات printArray() هنا قمنا باستدعاء الدالة
                    printArray( arr1 );
                    printArray( arr2 );
                    printArray( arr3 );
                    printArray( arr4 );
                    printArray( arr5 );
                    printArray( arr6 );

                    }

                    }
                  

سنحصل على النتيجة التالية عند التشغيل.

                    1 2 3 4 5
                    1 2 3 4 5
                    1.0 2.0 3.0 4.0 5.0
                    1.0 2.0 3.0 4.0 5.0
                    a b c d e
                    Generics are easy to understand 
                  
&&&////////////////////&&&/////////////////&&&

 مثال حول تعريف Generic Class و إنشاء كائن منه في جافا

في المثال التالي قمنا بتعريف كلاس إسمه Box, يملك نوع بيانات مجهول رمزنا له بالحرف T.
في هذا الكلاس قمنا بتعريف متغير إسمه x نوعه T.

ثم قمنا ببناء دوال للتعامل مع المتغير x:

  • الدالة getX()تستخدم لجلب قيمة المتغير x.

  • الدالة setX()تستخدم لتحديد قيمة المتغير x.

في الأخير قمنا بإنشاء الكلاس Main لتجربة إنشاء كائنات من الكلاس Box.


إنتبه

عند إنشاء كائن من الكلاس Box في حال لم تقم بوضع كلاس مكان الحرف <T> سيتم إعتبار x من النوع Object.
أما في حال قمت بوضع كلاس مكان الحرف <T>, سيتم تبديل جميع الأحرف T الموجودة في الكلاس Box بإسم الكلاس الذي أدخلته مكانها.


Box.java
                    public class Box <T> {

                    private T x;

                    public void set(T x) {
                    this.x = x;
                    }

                    public T get() {
                    return x;
                    }

                    }
                  

Main.java
                    public class Main {

                    public static void main(String[] args) {

                    // يمكنه تخزين أي نوع بيانات objectBox إسمه Box هنا قمنا بتعريف كائن من الكلاس
                    Box objectBox = new Box();

                    // Object ملاحظة سيتم تخزين النص كـ .x سيتم تخزين هذه القيمة في المتغير .String هنا أدخلنا فيه قيمة نوعها
                    objectBox.set( "object type can store any type of data" );

                    // أيضاً Object القيمة التي سترجع هنا يكون نوعها .x هنا قمنا بإرجاع القيمة التي تم تخزينها في المتغير
                    System.out.println("objectBox contains: " + objectBox.get());

                    // أيضاً Object مكان القيمة السابقة, و سيتم تخزينها كـ x سيتم تخزين هذه القيمة في المتغير .Integer هنا أدخلنا فيه قيمة نوعها
                    objectBox.set( 1234 );

                    // أيضاً Object القيمة التي سترجع هنا يكون نوعها .x هنا قمنا بإرجاع القيمة التي تم تخزينها في المتغير
                    System.out.println("objectBox contains: " + objectBox.get());



                    // Integer يمكنه فقط تخزين قيم نوعها integerBox إسمه Box هنا قمنا بتعريف كائن من الكلاس
                    Box<Integer> integerBox = new Box();

                    // أيضاً Integer و الذي أصبح نوعه x سيتم تخزين هذه القيمة في المتغير .Integer هنا قمنا بإدخال قيمة فيه نوعها
                    integerBox.set( 100 );

                    // أيضاً Integer القيمة التي سترجع هنا يكون نوعها .x هنا قمنا بإرجاع القيمة التي تم تخزينها في المتغير
                    System.out.println("integerBox contains: " + integerBox.get());



                    // String يمكنه فقط تخزين قيم نوعها stringBox إسمه Box هنا قمنا بتعريف كائن من الكلاس
                    Box<String> stringBox = new Box();

                    // أيضاً String و الذي أصبح نوعه x سيتم تخزين هذه القيمة في المتغير .String هنا قمنا بإدخال قيمة فيه نوعها
                    stringBox.set( "Hello, my type is String" );

                    // أيضاً String القيمة التي سترجع هنا يكون نوعها .x هنا قمنا بإرجاع القيمة التي تم تخزينها في المتغير
                    System.out.println("stringBox contains: " + stringBox.get());

                    }

                    }
                  

سنحصل على النتيجة التالية عند التشغيل.

                    objectBox contains: object type can store any type of data
                    objectBox contains: 1234
                    integerBox contains: 100
                    stringBox contains: Hello, my type is String
                  

إذاً نقوم بتعريف الكلاس كـ Generic عندما يكون عندنا عمليات في هذا الكلاس تطبق على أكثر من نوع.
فبدل إنشاء كلاس للتعامل مع كل نوع على حدة, نعرف الكلاس كـ Generic , و نحدد النوع الذي نريد التعامل معه أثناء إنشاء كائن من هذا الكلاس.

&&&////////////////////&&&/////////////////&&&

 مثال حول Bounded Type Parameters في جافا

في المثال التالي قمنا بتعريف دالتين إسمهم maximum, أي فعلنا Overload هنا.

الدالة الأولى نعطيها رقمين من أي نوع كان سواء (int - long - float - double ) فتقوم بإرجاع العدد الأكبر بينهما.
الدالة الثانية نعطيها ثلاث أرقام من أي نوع كان أيضاً فتقوم بإرجاع العدد الأكبر بينهما.

ملاحظة: جعلنا الدالة الثانية تستدعي الدالة الأولى مرتين بدل كتابتها من الصفر.


Main.java
                    public class Main {

                    // هنا قمنا ببناء دالة نعطيها رقمين من أي نوع, فتقوم بمقارنتهما و ترجع الرقم الأكبر
                    public static <N extends Number> N max (N x, N y)
                    {
                    // حتى نستطيع مقارنتهم, بعدها سيتم إرجاع العدد الأكبر بينهما double هنا سيتم تحويل نوع الأرقام المدخلة إلى النوع
                    if ( x.doubleValue() > y.doubleValue() )
                    return x;
                    else
                    return y;
                    }


                    // هنا قمنا ببناء دالة مشابهة للدالة السابقة, لكنها تقارن ثلاث أرقام مع بعض
                    public static <N extends Number> N max (N x, N y, N z)
                    {
                    // بكل بساطة, تستدعي الدالة السابقة لمقارنة أول عددين, و معرفة الأكبر بينهما
                    // بعدها تستدعيها من جديد لمقارنة العدد الأكبر بين العددين السابقين مع العدد الأخير
                    return max( max(x, y), z );
                    }


                    public static void main(String[] args) {

                    // التي تأخذ باراميترين max() هنا قمنا بإجراء ثلاث عمليات مقارنة باستخدام الدالة

                    // int هنا قمنا بمقارنة رقمين نوعهما
                    System.out.println( max(3, 7) );

                    // long مع رقم نوعه float هنا قمنا بمقارنة رقم نوعه
                    System.out.println( max(5.5F, 8L) );

                    // long مع رقم نوعه float هنا قمنا بمقارنة رقم نوعه
                    System.out.println( max(6.9D, 8L) );

                    // التي تأخذ ثلاث باراميترات max() هنا قمنا بإجراء عمليتين مقارنة تعتمدان على الدالة

                    // int هنا قمنا بمقارنة ثلاث أرقام نوعهم
                    System.out.println( max(10, 15, 5) );

                    // double مع رقم نوعه float مع رقم نوعه int هنا قمنا رقم نوعه
                    System.out.println( max(11, 12.5F, 17.8D) );

                    }

                    }
                  

سنحصل على النتيجة التالية عند التشغيل.

                    7
                    8
                    8
                    15
                    17.8
                  

إذاً عندما نقول أن النوع المجهول يرث من كلاس محدد. فهذا يعني أن الكود مصمم للتعامل مع هذا الكلاس, بالإضافة إلى جميع الكلاسات التي ترث منه.

&&&////////////////////&&&/////////////////&&&
&&&////////////////////&&&/////////////////&&&