اذهب إلى المحتوى

مفهوم البرمجة المعممة Generic Programming


رضوى العربي

تُشير البرمجة المُعمَّمة generic programming إلى كتابة شيفرةٍ يُمكِن تطبيقها على أنواعٍ كثيرة من البيانات. كنا قد تعرَّضنا بمقال معالجة المصفوفات Arrays في جافا للمصفوفات الديناميكية، والتي يُمكِن عدّها بديلًا عن البرمجة المُعمَّمة، وكتبنا شيفرةً تَعمَل مع مصفوفةٍ ديناميكية من الأعداد الصحيحة. في الحقيقة، لا يُمكِن لتلك الشيفرة أن تَعمَل مع أي نوعٍ غير النوع int، رغم أن الشيفرة المصدرية للمصفوفات الديناميكية من النوع double أو String أو Color أو أي نوع بياناتٍ آخر هي نفسها تقريبًا باستثناء تبديل اسم النوع؛ وبالتالي يبدو من الحماقة إعادة كتابة نفس الشيفرة مرارًا وتكرارًا.

تُقدِّم جافا حلًا لتلك المشكلة يُعرَف باسم الأنواع ذات المعاملات غير مُحدَّدة النوع parameterized types. كما رأينا بمقال مفهوم المصفوفات الديناميكية (ArrayLists) في جافا، يُنفِّذ الصنف ArrayList المصفوفات الديناميكية، ونظرًا لكونه نوعًا ذا معاملاتٍ غير مُحدَّدة النوع، فستَجِد أنواعًا؛ مثل النوع ArrayList<String>‎ لتمثيل مصفوفةٍ ديناميكيةٍ من السلاسل النصية من النوع String؛ والنوع ArrayList<Color>‎ لتمثيل مصفوفة ديناميكية من الألوان؛ والنوع ArrayList<T>‎ لأي نوع كائن T. لاحِظ أن ArrayList هي صنفٌ واحدٌ فقط، ولكنه مُعرَّف ليَعمَل مع أنواعٍ مختلفةٍ كثيرة، وهذا هو صميم البرمجة المُعمَّمة.

الصنف ArrayList هو في الواقع مجرد صنفٍ واحدٍ ضمن عددٍ كبيرٍ من أصناف جافا القياسية standard classes التي تَستخدِم البرمجة المُعمَّمة. سنناقش خلال المقالات الثلاثة التالية بعضًا من تلك الأصناف، ونتعرَّف على طريقة استخدامها، كما سنتعرَّض لواجهات interfaces وتوابع methods مُعمَّمة. لاحِظ أن جميع الأصناف والواجهات التي سنناقشها خلال تلك الأقسام مُعرَّفةٌ بحزمة package java.util، لذلك ستحتاج إلى كتابة التعليمة import ببداية برامجك لتتمكّن من استخدامها.

سنرى في مقال لاحق من هذه السلسلة إمكانية تعريف define أصنافٍ وواجهاتٍ وتوابعٍ مُعمَّمة، ولكن لحين وصولنا إلى تلك الجزئية، سنكتفي بأصناف جافا المُعمَّمة المُعرَّفة مُسبقًا. أخيرًا، سنفحص مجاري التدفق streams بالقسم قادم أيضًا؛ وهي خاصيةٌ جديدةٌ نسبيًا بلغة جافا، وتَستخدِم البرمجة المُعمَّمة بكثرة.

لا يُعدّ تصميم مكتبةٍ للبرمجة المُعمَّمة أمرًا سهلًا؛ في حين يتميز التصميم الذي تقترحه جافا لتنفيذ هذا النوع من البرمجة بالكثير من الخاصيات الرائعة، إلا أنه ليس الأسلوب الوحيد الممكن، وهو بالتأكيد ليس الحل الأمثل؛ حيث يُعاني -من وجهة نظر الكاتب- من بعض الخاصيات الشاذة، والتي قد تكون مع ذلك أفضل ما يمكن تحقيقه بالنظر إلى التصميم العام للغة جافا. سنَمُرّ الآن سريعًا على بعض المناهج الأخرى لتنفيذ البرمجة المُعمّمة، لتُكوِّن منظورًا عامًا عنها.

البرمجة المعممة بلغة Smalltalk

تُعدّ لغة Smalltalk واحدةً من أولى لغات البرمجة كائنية التوجه object-oriented، حيث لا تزال مُستخدَمةً حاليًا، ولكنها ليست شائعة. على الرغم من عدم وصولها إلى شعبية لغاتٍ، مثل Java و ++C، إلا أنها ألهمت الكثير من الأفكار المُستخدَمة بتلك اللغات. تُعد البرمجة بلغة Smalltalk مُعمَّمة بصورةٍ أساسيية نظرًا لتمتُّعها بخاصيتين أساسيتين، هما:

  • ليس المُتغيَّرات بلغة Smalltalk نوعًا؛ أي أن قيم البيانات لها نوعٌ مثل عددٍ صحيح أو سلسلة نصية، ولكن المُتغيَّرات ليس لها نوع، حيث يُمكِن لمُتغيِّر مُعين أن يَحمِل قيمًا بيانيةُ من أي نوع. وبالمثل، لا يوجد للمُعاملات parameters نوعًا، أي يُمكِن تطبيق برنامجٍ فرعي subroutine معينٍ على قيم معاملاتٍ من أي نوع؛ كما يُمكِن لبنيةٍ بيانيةٍ data structure مُعينةٍ أن تَحمِل قيمًا بيانيةً من أي نوع. حيث يمكنك مثلًا وبمجرد تعريف بنية شجرةٍ ثنائيةٍ معينة بلغة Smalltalk، استخدامها مع الأعداد الصحيحة، أو السلاسل النصية، أو التواريخ، أو أي بياناتٍ من أي نوعٍ آخر؛ أي ليس هناك حاجةً لكتابة شيفرةٍ جديدةٍ لكل نوعٍ بياني.

  • تُعدّ أي قيمةٍ بيانيةٍ بمثابة كائن object، كما أن جميع العمليات المُمكِن تطبيقها على الكائنات مُعرَّفةٌ مثل توابع methods ضِمْن صنفٍ معين، ويَنطبِق الأمر نفسه على جميع الأنواع الأساسية primitive بلغة جافا، مثل الأعداد الصحيحة. يعني ذلك أنه عند استخدِام العامل + مثلًا لجمع عددين صحيحين، تُنفَّذ العملية باستدعاء التابع المُعرَّف ضمن الصنف المُمثِل للأعداد الصحيحة. وبالمثل، عندما تُعرِّف صنفًا جديدًا، يُمكِنك تعرّيف العامل + ضمنه، ويمكنك بالتالي كتابة a + b لجمع كائناتٍ تنتمي إلى ذلك الصنف تمامًا مثلما تجمع أعدادًا.

لنفترض الآن أنك تَكتُب برنامجًا فرعيًا يَستخدِم العامل + لجمع العناصر الموجودة ضمن قائمة list، حيث تستطيع أن تُطبِق ذلك البرنامج على قائمة أعدادٍ صحيحة أو على قائمةٍ من أي نوعٍ آخر يَتَضمَّن تعريفًا للعامل +. يكون الأمر نفسه إذا عرَّفت برنامجًا فرعيًا يَستخدِم العامل > لترتيب قائمة، حيث يُمكِنك تطبيقه على قوائمٍ تحتوي على أي نوعٍ من البيانات طالما يَتَضمَّن ذلك النوع تعريفًا للعامل >، أي ليس هناك حاجةً لكتابة برنامجٍ فرعي لكل نوعٍ مختلفٍ من البيانات.

إذا توفَّرت الخاصيتان السابقتان ضمن لغةٍ معينة، يُمكِننا تطبيق الخوارزميات وبنى البيانات على أي نوعٍ من البيانات طالما كانت العمليات المناسبة مُعرَّفةً ضمن تلك الأنواع، حيث يُمثِل ذلك ماهية البرمجة المُعمَّمة. ربما ترى أن ذلك أمرًا مفيدًا دائمًا، وهو ما قد يدفعك إلى طرح السؤال التالي: لما لا تعمل كل اللغات البرمجية ببساطةٍ بنفس الأسلوب؟ في الحقيقة، في حين يُسهِل ذلك النوع من الحرية من كتابة البرامج، إلا أنه يُصعِّب مهمة كتابتها كتابةً صحيحةً متينة.

إذا كان لديك مثلًا بنيةً بيانيةً قادرةً على حمل أي نوعٍ من البيانات، ستَجِد أنه من الصعب التأكُّد من كونها تَحمِل فقط النوع المطلوب من البيانات؛ وبالمثل، إذا كان لديك برنامجًا فرعيًا يُمكِنه ترتيب أي نوعٍ من البيانات، ستَجِد أنه من الصعب التأكُّد من تطبيقه فقط على تلك الأنواع التي تتضمَّن تعريفًا للعامل >. ليس هناك طريقةً تُمكِّن المُصرِّف compiler من التأكُّد من مثل تلك الأشياء، وعليه ستَظهَر المشكلة فقط أثناء زمن التشغيل run time، أي عند محاولة تطبيق عمليةٍ معينةٍ على نوع بياناتٍ لا يتضمَّن تعريفًا لتلك العملية، وعندها سينهار crash البرنامج.

البرمجة المعممة بلغة C++‎

تُعدّ لغة C++‎ -بخلاف Smalltalk- لغةً صارمةً في تحديد النوع strongly typed، حيث يوجد لكل متغيرٍ variable نوعًا معينًا، ويُمكنه أن يَحمِل فقط قيمًا بيانيةً تنتمي إلى ذلك النوع؛ يعني ذلك أنه يستحيل تطبيق البرمجة المعمَّمة بنفس الكيفية المتوفرة بلغة Smalltalk. تتوفّر خاصية القوالب templates بلغة C++‎ مما يُمكِّنها من تطبيق نظامٍ آخر من البرمجة المُعمَّمة، حيث يُمكِنك ببساطةٍ كتابة قالب برنامجٍ فرعي subroutine template واحدٍ، بدلًا من كتابة برنامجٍ فرعي subroutine مختلف لترتيب كل نوعٍ من البيانات. لاحِظ أن القالب ليس برنامجًا فرعيًا، وإنما هو أشبه بمصنعٍ لإنشاء البرامج الفرعية. اُنظر المثال التالي:

template<class ItemType>
void sort( ItemType A[], int count ) {
      //1
   for (int i = count-1; i > 0; i--) {
      int position_of_max = 0;
      for (int j = 1; j <= i ; j++)
         if ( A[j] > A[position_of_max] )
            position_of_max = j;
      ItemType temp = A[i];
      A[i] = A[position_of_max];
      A[position_of_max] = temp;
   }
}

حيث تعني [1]: رتِّب عناصر المصفوفة A ترتيبًا تصاعديًا، حيث تُرتَّب العناصر الموجودة بالمواضع 0 و 1 و 2 وصولًا إلى count-1، وتُدعى الخوارزمية المُستخدَمة للترتيب باسم الترتيب الانتقائي selection sort.

تُعرِّف الشيفرة بالأعلى قالب برنامجٍ فرعي؛ فإذا حذفت السطر الأول template<class ItemType>‎، واستبدلت كلمة int بكلمة ItemType بباقي شيفرة القالب، فستَكون النتيجة برنامجًا فرعيًا subroutine بإمكانه ترتيب مصفوفاتٍ من الأعداد الصحيحة. يُمكِنك في الواقع استبدال أي نوعٍ بالنوع ItemType بما في ذلك الأنواع الأساسية primitive types، حيث يمكنك مثلًا استبدال كلمة string بكلمة ItemType للحصول على برنامج فرعي يُرتِّب مصفوفات سلاسل نصية.

هذه ببساطةٍ الطريقة التي يتعامل بها المُصرِّف مع أي قالب؛ فإذا كتبت sort(list,10)‎ بالبرنامج، حيث list مصفوفة أعدادٍ صحيحة من النوع int، فسيستخدم المُصرِّف القالب لتوليد برنامجٍ فرعي لترتيب مصفوفة أعدادٍ صحيحة؛ إذا كتبت sort(cards,10)‎، حيث cards هي مصفوفة كائناتٍ objects من النوع Card، فسيولّد المُصرِّف برنامجًا فرعيًا لترتيب مصفوفة كائناتٍ من النوع Card.

يَستخدِم القالب العامل > للموازنة بين القيم؛ فإذا كان ذلك العامل مُعرَّفًا للقيم من النوع Card، فسينجَح المُصرِّف بتوليد برنامجٍ فرعي لترتيب كانئات النوع Card بالاستعانة بالقالب؛ أما إذا لم يَكُن العامل > مُعرَّفًا للصنف Card، فسيَفشَل المُصرِّف أثناء زمن التصريف compile time وليس أثناء زمن التشغيل run time، مما يتسبَّب بانهيار البرنامج كما هو الحال بلغة Smalltalk. يُمكِنك كتابة تعريفاتٍ لعوامل مثل > لأي نوعٍ بلغة C++‎، أي يُمكِن للعامل > أن يَعمَل مع قيمٍ من النوع Card.

توفَّر C++‎ قوالبًا للأصناف بالإضافة إلى قوالب البرامج الفرعية subroutine templates؛ فإذا كتبت قالبًا لصنف شجرة ثنائية، فسيُمكِنك استخدِامه لإنشاء أصناف أشجارٍ ثنائيةٍ مُكوَّنةٍ من أعدادٍ صحيحةٍ من النوع int، أو سلاسل نصية من النوع string، أو تواريخ، أو غير ذلك وجميعها بنفس القالب. تأتي الإصدارات الأحدث من C++‎ مصحوبةً بعددٍ كبيرٍ من القوالب المكتوبة مُسبقًا، فيما يُعرَف باسم مكتبة القوالب القياسية Standard Template Library -أو اختصارًا STL-، والتي يراها الكثير معقدةً للغاية، ولكنها تُعدُّ مع ذلك واحدةً من أكثر خاصيات C++‎ تشويقًا.

البرمجة المعممة بلغة جافا Java

مرّت البرمجة المُعمَّمة بلغة جافا بعدة مراحلٍ من التطوير، وفي حين لم تتضمَّن الإصدارات الأولى من اللغة خاصية الأنواع ذات المعامِلات غير محدَّدة النوع parameterized types، إلا أنها وفَّرت أصنافًا تُمثِل بنى البيانات data structures الشائعة؛ حيث صُمِّمت تلك الأصناف لتعمل مع النوع Object أي يُمكِنها أن تَحمِل أي نوعٍ من الكائنات objects.

لم تكن هناك أي طريقةٍ لتخصيص أو قصر أنواع الكائنات المسموح بتخزينها ضمن بنيةٍ بيانيةٍ معينة، حيث لم يَكُن الصنف ArrayList مبدئيًا ضمن الأنواع ذات المعاملات غير محدَّدة النوع، أي كان من الممكن لأي مُتغيِّر من الصنف ArrayList أن يحمل أي نوعٍ من الكائنات. إذا كان list مثلًا متغيرًا من النوع ArrayList، فسيعيد list.get(i)‎ قيمةً من النوع Object، وبالتالي إذا استخدم المبرمج المُتغيِّر list لتخزين سلاسلٍ نصية من النوع String، فسيتوجب عليه تحويل نوع type-cast القيمة المعادة من list.get(i)‎ إلى سلسلةٍ نصية من النوع String على النحو التالي:

String item = (String)list.get(i);

يقع ذلك تحت تصنيف البرمجة المُعمَّمة؛ حيث يُمكِننا بالنهاية استخدام صنفٍ واحدٍ فقط للعمل مع أي نوعٍ من الكائنات، ولكنه في الواقع أشبه بلغة Smalltalk منه بلغة C++‎. وكما هو الحال مع لغة Smalltalk، سينتج عن ذلك حدوث الأخطاء أثناء زمن التشغيل لا زمن التصريف compile time؛ فإذا افترض مبرمجٌ مثلًا أن جميع العناصر الموجودة ضمن بنيةٍ بيانيةٍ معينة هي سلاسلٌ نصية strings، وحاول معالجتها بناءً على ذلك الأساس؛ فسيقع خطأٌ أثناء زمن التشغيل run time إذا احتوت تلك البنية على نوعٍ آخر من البيانات. عندما يحاول البرنامج أن يُحوِّل type-cast نوع قيمةٍ واقعةٍ ضمن بنيةٍ بيانيةٍ data structure إلى النوع String، سيقع خطأ من النوع ClassCastException بلغة جافا إذا لم تكن تلك القيمة من النوع String بالأساس.

أضاف الإصدار الخامس من جافا خاصية الأنواع ذات المعاملات غير محدَّدة النوع، مما ساعد على إنشاء بنى بياناتٍ مُعمَّمة generic data structures يُمكِن فحص نوعها أثناء زمن التصريف لا زمن التشغيل. إذا كانت list مثلًا قائمةٌ من النوع ArrayList<String>‎، فسيَسمَح المُصرِّف بإضافة الكائنات التي تنتمي إلى النوع String فقط إلى تلك القائمة list. علاوةً على ذلك، تَكون القيمة المعادة من استدعاء list.get(i)‎ من النوع String، ولهذا ليس من الضروري تحويلها إلى النوع الفعلي type-casting.

تُعدّ الأصناف ذات المعاملات غير محدَّدة النوع بلغة جافا شبيهةً نوعًا ما بأصناف القوالب template classes بلغة C++‎، على الرغم من اختلاف طريقة التنفيذ. سنعتمد خلال هذا الفصل على تلك الأصناف، ولكن عليك أن تتذكَّر بأن استخدام تلك المعاملات ليس إلزاميًا عند التعامل مع تلك الأصناف، فما يزال بإمكانك استخدام صنفٍ ذو معاملاتٍ غير محدَّدة النوع كما لو لم يَكُن كذلك، مثل كتابة ArrayList، حيث يُمكِن لأي كائنٍ أن يُخزَّن ضمنه في تلك الحالة؛ وإذا كان ذلك ما تريده حقًا، فمن الأفضل كتابة النوع ArrayList<Object>‎.

لاحِظ وجود فرقٍ كبير بين الأصناف ذات المعاملات غير محدَّدة النوع بلغة جافا Java وبين أصناف القوالب بلغة C++‎؛ حيث لا تُعدّ أصناف القوالب بلغة C++‎ أصنافًا من الأساس، ولكنها بمثابة مصانعٍ لإنشاء الأصناف. في كل مرةٍ نَستخدِم خلالها صنف قالبٍ template class معين مع نوعٍ جديد، فسيُصرِّف المُصرِّف صنفًا جديدًا؛ أما بالنسبة للغة جافا، فهناك ملف صنف مُصرَّف وحيد لكل صنفٍ ذو معاملاتٍ غير مُحدَّدة النوع، حيث يوجد مثلًا ملف صنفٍ وحيدٍ اسمه ArrayList.class للصنف ArrayList، وتَستخدِم أنواعٌ، مثل ArrayList<String>‎ و ArrayList<Integer>‎ نفس ذلك الملف المُصرَّف كما هو الحال مع النوع ArrayList العادي.

يقتصر دور معامل النوع type parameter، مثل String أو Integer في الواقع على تبليغ المُصرِّف بوجوب تقييد نوع الكائنات المسموح بتخزينها ضمن تلك البنية البيانية، وليس له أي تأثيرٍ خلال زمن التشغيل، حيث يُقال عندها أن معلومة النوع قد حُذِفَت أثناء زمن التشغيل run time، مما يؤدي إلى الكثير من الأمور الغريبة نوعًا ما. لا يُمكِنك مثلًا فحص اختبارٍ مثل:

if (list instanceof ArrayList<String>)‎

لأن قيمة العامل instanceof تُحصَّل أثناء زمن التشغيل، ولا يوجد سوى الصنف العادي ArrayList أثناء زمن التشغيل. ولا يُمكِنك أيضًا إجراء تحويلٍ بين الأنواع type-cast إلى ArrayList<String>‎، بل حتى لا تستطيع استخدام العامل new لإنشاء مصفوفةٍ نوعها الأساسي base type هو ArrayList<String>‎، كأن تَكْتُب:

new ArrayList<String>[N]‎

لأن العامل new يُحصَّل أثناء زمن التشغيل، ولا يكون هناك شيءٌ اسمه ArrayList<String>‎ في ذلك الوقت كما ذكرنا مُسبَقًا، وإنما فقط النوع العادي ArrayList بدون معاملاتٍ غير مُحدَّدة النوع non-parameterized.

على الرغم من عدم القدرة على إنشاء مصفوفةٍ من النوع ArrayList<String>‎، يُمكِنك إنشاء قائمةٍ من النوع ArrayList تحتوي على قائمةٍ أخرى من النوع ArrayList<String>‎، ويُكتَب النوع على النحو التالي:

ArrayList<ArrayList<String>>‎ 

تظهَر تلك المشكلات لحسن الحظ فقط بالبرمجة المتقدمة نوعًا ما، حيث لا يُواجِه غالبية المبرمجين الذين يَستخدِمون الأصناف ذات المعاملات غير محدَّدة النوع تلك المشكلات، ويُمكِنهم الاستفادة من نموذج البرمجة المُعمَّمة وبأنواع بياناتٍ آمنة type-safe دون أي صعوبة.

لاحِظ أنه في حالة كان المُصرِّف قادرًا على استنتاج اسم معامل النوع type parameter المُستخدَم بصنفٍ ذي معاملاتٍ غير مُحدَّدة النوع، يمكن عندها حَذْف اسم معامل النوع. نظرًا لأن المصفوفة المُنشأة في المثال التالي ستكون حتمًا من النوع ArrayList<String>‎ لتتوافق مع نوع المُتغيِّر، فمن الممكن حَذْف كلمة String بتعليمة الباني constructor على النحو التالي:

ArrayList<String> words = new ArrayList<>();

إطار جافا للتجميعات

تُوفِّر جافا عدة أنواعٍ ذات معاملاتٍ غير مُحدَّدة النوع لتمثيل بنى البيانات الشائعة، حيث يُشار عادةً إلى تلك المجموعة من الأصناف والواجهات interfaces باسم إطار عمل جافا للتجميعات Java Collection Framework -أو اختصارًا JCF-، والتي سنناقشها خلال الأقسام القليلة التالية.

يُمكِننا تقسيم بنى البيانات المُعمَّمة generic بإطار عمل جافا للتجميعات إلى تصنيفين، هما التجميعات collections والخرائط maps؛ حيث تشير التجميعة ببساطة إلى تجميعةٍ من الكائنات؛ أما الخريطة فهي تَربُط كائنات مجموعة بكائنات مجموعةٍ أخرى بنفس الأسلوب الذي يَربُط به القاموس التعريفات بالكلمات، أو يَربُط به دليل الهاتف أرقام الهواتف بأسماء الأشخاص.

تُشبِه الخريطة القوائم المترابطة association list، التي ناقشناها بمقال البحث والترتيب في المصفوفات Array في جافا؛ حيث تُمثِل الواجهتين Collection<T>‎ و Map<T,S>‎ ذواتا المعاملات غير مُحدَّدة النوع التجميعات والخرائط بلغة جافا، بحيث تُشير T و S إلى أي نوعٍ باستثناء الأنواع الأساسية. تُعدّ الواجهة Map<T,S>‎ مثالًا على الأنواع ذات المعاملات غير مُحدَّدة النوع، ويَملُك تحديدًا معاملي نوع type parameters، هما T و S. سنتناول خلال هذا المقال التجميعات، بينما سنناقش الخرائط تفصيليًا في مقال قادم.

تُقسم التجميعات بدورها إلى نوعين، هما القوائم lists والأطقم sets؛ حيث تُخزِّن القوائم الكائنات الموجودة بها وفقًا لتسلسلٍ خطيٍ معين، وهذا يَعنِي أنه يُمكِننا الإشارة إلى العنصر الأول أو الثاني الموجود ضمن قائمةٍ معينة. علاوةً على ذلك، لا بُدّ أن يَتبَع أي عنصرٍ ضمن القائمة باستثناء العنصر الأخير عنصرًا آخرًا. في المقابل، لا يُمكِن لأي طقمٍ set أن يتضمَّن نفس الكائن أكثر من مرة، ولا تُعدّ العناصر الموجودة به مُرتَّبةً وفقًا لأي ترتيب، أي لا ينبغي أن تفكر بها على هذا الأساس.

تُمثِل الواجهتان List<T>‎ و Set<T>‎ القوائم والأطقم على الترتيب، وهما مُشتقتان من الواجهة Collection<T>‎، وهذا يَعنِي أن الكائنات المُنفِّذة للواجهة List<T>‎ أو Set<T>‎ تُنفِّذ الواجهة Collection<T>‎ أيضًا على نحوٍ تلقائي. تُخصِّص الواجهة Collection<T>‎ العمليات العامة المُمكن تطبيقها على أي تجميعة؛ بينما تُخصِّص الواجهتان List<T>‎ أو Set<T>‎ أي عملياتٍ إضافيةٍ أخرى ضروريةً للقوائم والأطقم على الترتيب.

لاحِظ أن أي كائن فعليّ سواءٌ كان تجميعةً، أو قائمةً، أو طقمًا set، لا بُدّ أن ينتمي إلى صنفٍ حقيقي concrete class يُنفِّذ الواجهة المقابلة. يُنفِّذ الصنف ArrayList<T>‎ الواجهة List<T>‎ على سبيل المثال، ويُنفِّذ بالتالي Collection<T>‎؛ وهذا يعني أننا نستطيع استخدام جميع التوابع المُعرَّفة بواجهتي القوائم والتجميعات مع النوع ArrayList. سنفحص أصنافًا مختلفة تُنفِّذ واجهتي الطقم والقائمة بالمقال التالي، ولكن قبل أن نفعل ذلك، سنناقش سريعًا بعضًا من العمليات العامة المتاحة بأي تجميعة.

تُخصِّص الواجهة Collection<T>‎ توابعًا لإجراء عددٍ من العمليات الأساسية على أي تجميعةٍ من الكائنات. بما أن التجميعة مفهومٌ عام، فإن العمليات التي يُمكِن تطبيقها عليها في العموم عامةٌ أيضًا؛ وهذا يعني أنها عملياتٌ مُعمَّمة أي قابلة للتطبيق على أنواعٍ مختلفة من التجميعات التي تحتوي بدورها على أنواعٍ مختلفة من الكائنات. فإذا كان coll كائنًا يُنفِّذ الواجهة Collection<T>‎ على سبيل المثال، تَكُون العمليات التالية مُعرَّفةً له:

  • coll.size()‎: تُعيد عددًا صحيحًا من النوع int يُمثِل عدد الكائنات الموجودة بالتجميعة.
  • coll.isEmpty()‎: تُعيد قيمةً من النوع المنطقي boolean، حيث تَكُون مساويةً للقيمة true إذا كان حجم التجميعة يُساوي الصفر.
  • coll.clear()‎: تَحذِف جميع الكائنات الموجودة بالتجميعة.
  • coll.add(tobject)‎: تُضيف tobject إلى التجميعة. لا بُدّ أن يَكون المعامل من النوع T، وإلا سيَحدُث خطأ في بناء الجملة syntax error أثناء زمن التصريف compile time. إذا كان T صنف، فإنه يَشمَل جميع الكائنات التي تنتمي لأي صنفٍ فرعي subclass مُشتقٍّ من T؛ أما إذا كان T واجهة، فإنه يَتضمَّن أي كائنٍ مُنفّذٍ لتلك الواجهة T. يُعيد التابع add()‎ قيمةً من النوع المنطقي تُمثِّل فيما إذا كان التابع قد أجرى تعديلًا فعليًا على التجميعة أم لا؛ فإذا أضفت كائنًا إلى طقمٍ set معين، وكان ذلك الكائن موجودًا بالفعل ضمن ذلك الطقم، لا يكون للتابع أي تأثير.
  • coll.contains(object)‎: تُعيد قيمةً من النوع المنطقي، والتي تكون مساويةً القيمة true إذا كان object موجودًا بالتجميعة. لاحِظ أنه من غير الضروري أن يكون المُعامل object من النوع T؛ فقد ترغب بفحص ما إذا كان object موجودًا ضمن التجميعة بغض النظر عن نوعه. بالنسبة لعملية اختبار التساوي، تُعدّ القيمة الفارغة null مُساويةً لنفسها؛ أما بالنسبة للكائنات غير الفارغة، يختلف المقياس الذي يُحدَّد على أساسه تَساوِي تلك الكائنات من عدمه من نوع تجميعةٍ إلى آخر.
  • coll.remove(object)‎: تَحذِف object من التجميعة إذا كان موجودًا بها، وتُعيد قيمةً من النوع المنطقي تُحدِّد فيما إذا كان التابع قد عثر على object ضمن التجميعة أم لا. ليس من الضروري أن يكون المعامل object من النوع T. تُجرَى عملية اختبار التساوي بنفس الطريقة المُتبعَّة بالتابع contains.
  • coll.containsAll(coll2)‎: تُعيد قيمةً منطقيةً تَكُون مساويةً للقيمة true إذا كانت جميع كائنات التجميعة coll2 موجودةً أيضًا بالتجميعة coll، ويُمكِن للمعامل coll2 أن ينتمي لأي نوع تجميعة.
  • coll.addAll(coll2)‎: تُضيف جميع كائنات التجميعة coll2 إلى coll، حيث يُمكِن للمعامل coll2 أن يُمثِل أي تجميعةٍ من النوع Collection<T>‎، ولكنه قد يكون أيضًا أعم من ذلك. على سبيل المثال، إذا كان T صنف و S صنفٌ فرعي subclass مُشتقٌ من T، فقد يكون coll2 من النوع Collection<S>‎، وهو أمرٌ منطقي لأن أي كائن من النوع S هو بالضرورة من النوع T، وبالتالي يُمكِن إضافته إلى coll.
  • coll.removeAll(coll2)‎: تَحذِف أي كائنٍ من التجميعة coll إذا كان موجودًا أيضًا بالتجميعة coll2، حيث يُمكِن للمعامل coll2 أن ينتمي لأي نوع تجميعة.
  • coll.retainAll(coll2)‎: تَحذِف أي كائنٍ من التجميعة coll إذا لم يَكُن موجودًا بالتجميعة coll2؛ أي أنها تُبقِي فقط الكائنات غير الموجودة بالتجميعة coll2، ويُمكِن للمعامل coll2 أن ينتمي لأي نوع تجميعة.
  • coll.toArray()‎: تُعيد مصفوفةً من النوع Object[]‎ تحتوي على جميع عناصر التجميعة. نلاحظ أن النوع المُعاد من التابع هو Object[]‎ وليس T[]‎. هناك في الواقع نسخةٌ أخرى من نفس التابع coll.toArray(tarray)‎، والتي تَستقبِل مصفوفةً من النوع T[]‎ مثل مُعامل وتُعيد مصفوفةً من النوع T[]‎ تحتوي على جميع عناصر التجميعة. في حال كانت المصفوفة المُمرَّرة tarray كبيرةً بما يكفي لحَمْل جميع عناصر التجميعة، فسيُخزَّن التابع العناصرأيضًا بنفس المصفوفة المُمرَّرة ويُعيدها مثل قيمة للتابع؛ أما إذا لم تَكُن المصفوفة كبيرة بما يكفي، يُنشِئ التابع مصفوفةً جديدةً لحمل تلك العناصر، ويَقتصِر في تلك الحالة دور المصفوفة المُمرَّرة tarray على تحديد نوع المصفوفة المُعادة فقط. يُمكِننا مثلًا استدعِاء coll.toArray(new String[0])‎ إذا كان coll تجميعةً من السلاسل النصية من النوع String، وسيُعيد الاستدعاء السابق بناءً على ذلك مصفوفةً جديدةً من النوع String.

طالما أنّ التوابع السابقة مُعرَّفةٌ ضمن الواجهة Collection<T>‎، فإنها بطبيعة الحال مُعرَّفةٌ ضمْن كل كائنٍ يُنفِّذ تلك الواجهة، ولكن هناك مشكلة، حيث لا يُمكِن تغيير حجم بعض أنواع التجميعات بعد إنشائها، وبالتالي لا يَكون للتوابع المسؤولة عن إضافة الكائنات وحذفها معنىً بالنسبة لتلك التجميعات على الرغم من أنه ما يزال من الممكن استدعائها، ويَحدُث في تلك الحالة اعتراضٌ exception من النوع UnsupportedOperationException أثناء زمن التشغيل.

إضافةً لذلك ونظرًا لأن Collection<T>‎ هي واجهة interface وليست صنفًا حقيقيًا concrete class، فإن التنفيذ الفعلي للتابع متروكٌ للأصناف المُنفِّذة للواجهة؛ مما يعني عدم امكانية ضمَان توافق الدلالة الفعلية لتلك التوابع مع ما شرحناه بالأعلى لجميع تجميعات الكائنات من خارج إطار عمل جافا للتجميعات Java Collection Framework.

بالنسبة لكفاءة تلك العمليات، فليس من الضروري أن تعمل عمليةٌ معينةٌ بنفس كفاءة أنواعٍ مختلفةٍ من التجميعات؛ حيث أنها بالنهاية مُعرَّفةٌ بكل تجميعةٍ على حدى. يَنطبِق ذلك حتى على أبسط التوابع مثل size()‎التي فقد تختلف كفائتها تمامًا من تجميعةٍ لأخرى؛ حيث من الممكن أن يتضمَّن تحصيل قيمة التابع size()‎ عَدّ العناصر الموجودة بالتجميعة بالنسبة لبعض أنواع التجميعات، ويكون عندها عدد خطوات العملية مُساويًا لعدد عناصر التجميعة؛ وقد يحتفظ نوعٌ آخر من التجميعات بمتغيرات نسخ instance variables تُحدِّد حجمها الحالي، وعندها يقتصر تحصيل قيمة التابع size()‎ على إعادة قيمة مُتغيِّر، أي يَستغرِق تنفيذ العملية خطوةً واحدةً فقط بغض النظر عن عدد عناصر التجميعة. بناءً على ما سبق، لا بُدّ من الانتباه دائمًا لكفاءة العمليات، واختيار التجميعة بحيث تكون العمليات التي ستجريها أكثر من غيرها ذات الكفاءة الأعلى، وسنرى عدة أمثلة على ذلك في المقالين التاليين.

المكررات وحلقات التكرار for-each

تُعرِّف الواجهة Collection<T>‎ بعض الخوارزميات المُعمَّمة البسيطة، ولكن كيف يختلف ذلك عن كتابة خوارزميةٍ مُعمَّمةٍ خاصةٍ جديدة؟ لنفترض مثلًا أننا نريد طباعة جميع العناصر الموجودة ضمن التجميعة. لنُنفِّذ ذلك تنفيذًا مُعمَّمًا، نحتاج إلى طريقةٍ ما للمرور عبر جميع عناصر التجميعة واحدًا تلو الآخر. رأينا طريقة فعل ذلك لبعض بنى البيانات data structure؛ فإذا كان لدينا مصفوفةٌ مثلًا، فإننا نستطيع ببساطة استخدام حلقة التكرار for للمرور عبر جميع فهارسها indices. تُعدُّ القائمة المترابطة linked list مثالًا آخر، حيث يُمكِننا المرور عبر عناصرها باستخدام حلقة التكرار while، بحيث نُحرِّك ضمن تلك الحلقة مؤشرًا على طول القائمة.

بالنسبة للشجرة الثنائية binary tree، يُمكِننا استخدام برنامجٍ فرعيٍ تعاودي recursive لإجراء ما يُعرَف باسم اجتياز في الترتيب inorder traversal؛ أما بالنسبة للتجميعة collection، فيمكننا تمثيلها بأيٍ مما سبق، وبالتالي علينا الإجابة عن السؤال التالي: كيف سنستطيع كتابة تابعٍ مُعمَّمٍ واحدٍ يُمكِنه العمل مع تجميعاتٍ يُمكِن تخزينها بصيغٍ مختلفةٍ تمامًا؟ يَكْمُن حل تلك المشكلة فيما يُعرَف باسم المُكرِّرات iterators؛ وهو ببساطةٍ كائنٌ يُمكِن استخدامه لاجتياز تجميعة.

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

تُعرِّف الواجهة Collection<T>‎ تابعًا يُمكِننا استخدامه للحصول على المُكرِّر iterator لأي تجميعة. إذا كانت coll تجميعة، فسيعيد coll.iterator()‎ مُكرِّرًا يُمكِننا اِستخدَامه لاجتياز عناصر التجميعة. يُمكِنك التفكير بالمُكرِّر كما لو كان نوعًا عامًا من المؤشرات يبدأ من مقدمة التجميعة، وبإمكانه التحرُّك على طول التجميعة من عنصرٍ إلى آخر. تُعرَّف المُكرِّرات عن طريق واجهةٍ ذات معاملات غير مُحدَّدة النوع parameterized interface اسمها Iterator<T>‎. إذا نفِّذت coll الواجهة Collection<T>‎ للنوع T، فسيعيد استدعاء coll.iterator()‎ مُكرِّرًا من النوع Iterator<T>‎، حيث تُشير T إلى معامل النوع type parameter. تُعرِّف الواجهة Iterator<T>‎ ثلاثة توابع فقط. إذا كان tier يُشير إلى كائن مُنفِّذ للواجهة Iterator<T>‎، يكون لدينا التوابع التالية:

  • iter.next()‎: يُعيد العنصر التالي، ويُقدِّم المُكرِّر خطوةً للأمام، وتكون القيمة المُعادة من النوع T. يسمح لك التابع بفحص أحد عناصر التجميعة. لاحِظ أنه لا توجد طريقةٌ لفحص عنصرٍ دون أن يَمُر المُكرِّر عبره خطوةً للأمام. إذا استدعينا هذا التابع ولم يَكن هناك أي عناصرٍ متبقية ضمن التجميعة، فسيحدث اعتراضٌ من النوع NoSuchElementException.
  • iter.hasNext()‎: يُعيد قيمةً منطقيةً تُمثِل فيما إذا كان هناك عناصرٌ جاهزةٌ متبقيةٌ للمعالجة. ينبغي استدعاء هذا التابع عمومًا قبل استدعاء iter.next()‎.
  • iter.remove()‎: إذا استدعيت هذا التابع بعد iter.next()‎، فسيحذِف العنصر الذي رأيته للتو من التجميعة. لا يستقبل هذا التابع أي مُعاملات، ويَحذِف آخر عنصرٍ أعاده التابع iter.next()‎، مما قد يؤدي إلى اعتراضٍ من النوع UnsupportedOperationException في حال لم تدعم تلك التجميعة حذف العناصر.

نستطيع كتابة شيفرة لطباعة كل العناصر الموجودة بأي تجميعة بالاستعانة بالمُكرِّرات iterators. لنفترض مثلًا أن coll من النوع Collection<String>‎، وبالتالي سيعيد التابع coll.iterator()‎ قيمةً من النوع Iterator<String>‎، ويُمكِننا كتابة ما يلي:

Iterator<String> iter;          // صرِّح عن المُكرِّر
iter = coll.iterator();         // استرجع مُكررًا للتجميعة
while ( iter.hasNext() ) {
   String item = iter.next(); // اقرأ العنصر التالي
   System.out.println(item);
}

ستَعمَل الصيغة العامة السابقة مع أي أنواعٍ أخرى من المعالجة، حيث تَحذِف الشيفرة التالية مثلًا جميع القيم الفارغة null من أي تجميعةٍ من النوع Collection<Color>‎، طالما كانت التجميعة تدعم حذف القيم:

Iterator<Color> iter = coll.iterator():
while ( iter.hasNext() ) {
    Color item = iter.next();
    if (item == null)
       iter.remove();
}

لاحِظ أنه عند استخدامنا أنواعًا، مثل Collection<T>‎، أو Iterator<T>‎، أو أي نوعٍ آخر ذا معاملاتٍ غير مُحدَّدة النوع ضمن شيفرةٍ فعلية، فإننا نستخدمها دائمًا مع أنواعٍ فعليةٍ، مثل String، أو Color في موضع معامل النوع الصوري T؛ حيث يُستخدَم مثلًا مُكرِّرٌ من النوع Iterator<String>‎ للمرور عبر عناصر تجميعة سلاسلٍ نصيةٍ من النوع String؛ بينما يُستخدَم مُكرِّرٌ من النوع Iterator<Color>‎ للمرور عبر عناصر تجميعةٍ من النوع Color وهكذا.

تُستخدَم المُكرِّرات عادةً لتطبيق نفس العملية على جميع عناصر تجميعةٍ معينة، ولكن يمكننا استخدام حلقة التكرار for-each بدلًا من المُكرِّر في كثيرٍ من الحالات. كنا قد ناقشنا طريقة استخدام حلقة for-each مع المصفوفات بمقال تعرف على المصفوفات (Arrays) في جافا، ومع النوع ArrayList بمقال  المشار إليه سلفًا، ويُمكِنها أن تُستخدَم أيضًا للمرور عبر عناصر أي تجميعة. على سبيل المثال، إذا كان coll تجميعةً من النوع Collection<T>‎، تُكْتَب حلقة for-each بالصياغة التالية:

for ( T x : coll ) { // ‫لكل كائن x من النوع T بالتجميعة coll
   // ‫عالج x
}

تمثِّل x بالأعلى مُتغيِّرًا مُتحكِّمًا بالحلقة loop control variable، وسيُسند كل كائنٍ بالتجميعة coll إلى x، وسيُطبَق متن body الحلقة عليه. صرَّحنا عن x لتَكون من النوع T، لأن الكائنات الموجودة بالتجميعة coll من النوع T. إذا كانت namelist تجميعةً من النوع Collection<String>‎ مثلًا، يُمكِننا طباعة جميع الأسماء الموجودة بالتجميعة على النحو التالي:

for ( String name : namelist ) { 
   System.out.println( name );
}

يُمكِننا بالطبع كْتابة حلقة while مع مُكرِّر بدلًا من حلقة for-each، ولكن الأخيرة أسهل بالقراءة.

التساوي Equality والموازنة Comparison

تتضمَّن الواجهة Collection عدة توابعٍ methods لفحص تَساوِي كائنين. يبحث التابعان coll.contains(object)‎ و coll.remove(object)‎ على سبيل المثال عن عنصرٍ يُساوِي object ضمن التجميعة. لا يُعد اختبار تساوي كائنين أمرًا بسيطًا كما قد تظنّ، ولا يُعطِي العامل == دائمًا إجاباتٍ معقولةً عند تطبيقه على الكائنات؛ لأنه يَفحَص فيما إذا كان الكائنان متطابقين أي إذا كانا بنفس موضع الذاكرة memory location، وهو ما لا نعنيه عادةً عندما نرغب بفْحَص تَساوِي كائنين، وإنما نَعنِي ما إذا كانا يحملان نفس القيمة، وهو أمرٌ مختلفٌ كليًا. إذا كان لدينا مثلًا قيمتين من النوع String، فلا بُدّ أن نَعُدّهما متساويين إذا تضمَّنا نفس متتالية المحارف بغض النظر عن وجودهما بنفس موضع الذاكرة من عدمه؛ وإذا كان لدينا قيمتين من النوع Date، فلا بُدّ أن نَعُدّهما متساويين إذا كانا يُمثِلان نفس التوقيت.

يُعرِّف الصنف Object تابعًا اسمه equals(Object)‎ بهدف فَحْص تساوي كائنين، وبحيث يؤدي نفس دور الاختبار التالي obj1 == obj2، ثم يُعيد قيمةً من النوع المنطقي boolean. تَستخدم كثيرًا من أصناف التجميعات ذلك التابع، ومع ذلك، لا يُعدّ هذا التعريف مناسبًا لكثيرٍ من الأصناف الفرعية المُشتقَّة من الصنف Object، وبالتالي يَنبغي أن يُعاد تعريفه overridden، حيث يُعيد الصنف String مثلًا تعريف التابع equals()‎ بحيث تَكون قيمة str.equals(obj)‎ لسلسلةٍ نصية str مساويةً للقيمة المنطقية true إذا كان obj من النوع String وكان يحتوي على نفس متتالية المحارف الموجود بالسلسلة النصية str.

إذا أضفت صنفًا جديدًا، فينبغي أن يحتوي تعريفه على إعادة تعريفٍ للتابع equals()‎ لتحصل على السلوك المطلوب عند فَحص تساوي كائنين من ذلك الصنف. يُمكِننا على سبيل المثال تعريف صنف Card على النحو التالي لنتمكَّن من اِستخدَامه داخل تجميعة collection:

public class Card {  // صنف لتمثيل ورق اللعب

   private int suit;  //  عدد من 0 إلى 3 لتمثيل بطاقات الكوبة والبستوني
                      // والسباتي والديناري
   private int value; // عدد من 1 إلى 13 لتمثيل قيمة الورقة

   public boolean equals(Object obj) {
       try {
          Card other = (Card)obj;  // Type-cast obj to a Card.
          if (suit == other.suit && value == other.value) {
            // 1
             return true;
          }
          else
             return false;
       }
       catch (Exception e) {
              // 2
           return false;
       }
    }

    .
    . // توابع أخرى وبناةٌ آخرين
    .
}

حيث تعني كل من:

  • [1] تملك الورقة الأخرى نفس القيمة والرمز الخاص بهذه الورقة، لذلك يُمكن عدّهما متساويين.
  • [2] سيلتقط الاعتراض NullPointerException الذي يَحدُث إذا كان obj فارغًا، وكذلك الاعتراض ClassCastException الذي يَحدُث إذا لم يَكُن obj من النوع Card. في تلك الحالات، لا يكون obj مُساوٍ لورقة اللعب 'Card'، لذلك أعِد false.

لاحِظ أنه في حالة عدم وجود التابع equals()‎ داخل الصنف، لن تعمل توابعٌ، مثل contains()‎ و remove()‎ المُعرَّفة بالواجهة Collection<Card>‎ على النحو المُتوقَّع.

يَنطبِق الأمر نفسه على عملية ترتيب العناصر الموجودة ضمن تجميعة، أي عملية ترتيبها تصاعديًا وفقًا لمعيارٍ معين. ليس هناك مفهومٌ بديهيٌ لمعنى الترتيب التصاعدي لعدِّة كائناتٍ objects عشوائية، ولهذا لا بُدّ من إضافة تابعٍ آخر قبل محاولة ترتيب تلك العناصر، بحيث يكون ذلك التابع مسؤولًا عن الموازنة بين تلك العناصر. يجب أن يُنفِّذ أيُّ كائنٍ تَنوِي استخدامه ضمن عملية موازنة الواجهة java.lang.Comparable. لاحِظ أن تلك الواجهة مُعرَّفةٌ مثل واجهةٍ ذات معاملات غير محدَّدة النوع Comparable<T>‎، مما يعني إمكانية الموازنة مع كائنٍ من النوع T. تُعرِّف الواجهة Comparable<T>‎ التابع التالي:

public int compareTo( T obj )

يُعيد الاستدعاء obj1.compareTo(obj2)‎ قيمةً سالبةً، إذا كان obj1 يَأتِي قبل obj2 عندما تكون الكائنات مُرتَّبة ترتيبًا تصاعديًا؛ ويُعيد قيمةً موجبةً، إذا كان obj2 يَأتِي قبل obj1؛ وإذا كان الكائنان مُتساوِيين وفقًا للغرض من الموازنة، يُعيد التابع صفرًا. لا يعني ذلك بالضرورة أن الكائنين مُتساويان وفقًا للتابع الآخر obj1.equals(obj2)‎. فإذا كانت الكائنات قيد الموازنة من النوع Address على سبيل المثال، فقد ترغب بترتيبها وفقًا للرقم البريدي، بحيث تَكُون العناوين ذات نفس الرقم البريدي مُتساوية، ولا يَعنِي ذلك أن تلك العناوين هي نفسها.

يُنفِّذ الصنف String الواجهة Comparable<String>‎، ويُعرِّف التابع compareTo بحيث يُعيد صفرًا فقط إذا كانت السلسلتان النصيتان قيد الموازنة متساويتين. إذا عرَّفت صنفًا خاصًا جديدًا، وكنت ترغب بترتيب الكائنات المنتمية لذلك الصنف، فينبغي أن تَفعَل الشيء نفسه. اُنظر المثال التالي:

// 4
public class FullName implements Comparable<FullName> {

   private String firstName, lastName;  // الاسم الأول والأخير غير الفارغين

   public FullName(String first, String last) {  // الباني
      if (first == null || last == null)
         throw new IllegalArgumentException("Names must be non-null.");
      firstName = first;
      lastName = last;
   }

   public boolean equals(Object obj) {
      try {
         FullName other = (FullName)obj;  // Type-cast obj to type FullName
         return firstName.equals(other.firstName) 
                                && lastName.equals(other.lastName);
      }
      catch (Exception e) {
         return false;  // ‫إذا كان `obj` فارغًا أو لم يكن من النوع FullName
      }
   }

   public int compareTo( FullName other ) {
      if ( lastName.compareTo(other.lastName) < 0 ) {
             // 1
         return -1;
      }
      else if ( lastName.compareTo(other.lastName) > 0 ) {
             // 2
         return 1;
      }
      else {
             // 3
         return firstName.compareTo(other.firstName);
      }
   }

   .
   . // توابع أخرى 
   .
}

وتشير العناصر الآتية إلى:

  • [1]: إذا جاء lastName قبل الاسم الأخير للكائن الآخر، فسيأتي FullName لهذا الكائن قبل FullName للكائن الآخر، ولذلك أعِد قيمةً سالبة.
  • [2]: إذا جاء lastName بعد الاسم الأخير للكائن الآخر، فسيأتي FullName لهذا الكائن بعد FullName للكائن الآخر، ولذلك أعد قيمةً موجبة.
  • [3]: الاسم الأخير لكلا الكائنين هو نفسه، ولذلك سنوازن بين أسمائهما الأولى باستخدام التابع compareTo المُعرَّف بالصنف String.
  • [4]: يمثِّل هذا الصنف الاسم الكامل المُكوَّن من اسمٍ أول واسمٍ أخير.

لاحِظ أن الصنف مُعرَّفٌ على النحو التالي class FullName implements Comparable<FullName>‎، وقد يبدو استخدام كلمة FullName مثل معامل نوع type parameter ضمن اسم الواجهة غريبًا بعض الشيء ولكنه صحيح؛ حيث يعني أننا ننوي موازنة الكائنات المنتمية إلى الصنف FullName مع كائناتٍ أخرى من نفس النوع. قد ترى أنه من البديهي أن تكون عملية الموازنة مع كائنٍ من نفس النوع، ولكنها ليست كذلك بالنسبة لمُصرِّف جافا، ولهذا أضفنا معامل النوع إلى اسم الواجهة على النحو التالي Comparable<FullName>‎.

تُوفِّر جافا طريقةً أخرى لموازنة الكائنات عبر إضافة كائنٍ آخر يكون قادرًا على إجراء الموازنة، ويجب على ذلك الكائن تنفيذ الواجهة Comparator<T>‎، حيث T هي نوع الكائنات المطلوب موازنتها. تُعرِّف تلك الواجهة التابع التالي:

public int compare( T obj1, T obj2 )

يُوازن التابع السابق كائنين من النوع T، حيث يُعيد قيمةً سالبةً أو موجبةً أو صفرًا اعتمادًا على ما إذا كان obj1 يَسبِق obj2، أو إذا كان obj1 يَلحَق obj2، أو إذا كان يُمكِن عَدّهما مُتساويين فيما يتعلق بالموازنة. تُستخدَم تلك الواجهة عادةً لموازنة الكائنات التي لا تُنفِّذ الواجهة Comparable، وكذلك لتخصيص أساليب ترتيبٍ مختلفة لنفس تجميعة الكائنات. لاحِظ أنه نظرًا لأن Comparator هو واجهة من نوع دالة functional interface، وتُستخدَم غالبًا تعبيرات لامدا lambda expressions لتعريفها (انظر مقال تعبيرات لامدا (Lambda Expressions) في جافا).

سنناقش خلال المقالين التاليين طريقة استخدام Comparable و Comparator بالتجميعات والخرائط.

الأنواع المعممة والأصناف المغلفة

لا يُمكِننا تطبيق نموذج البرمجة المُعمَّمة generic programming بلغة جافا على الأنواع الأساسية primitive types كما ذكرنا بمقال مفهوم المصفوفات الديناميكية (ArrayLists) في جافا. أثناء حديثنا عن الصنف ArrayList؛ حيث يمكن لبنى البيانات data structures المُعمَّمة أن تَحمِل كائناتٍ فقط وليست الأنواع الأساسية كائنات. تستطيع في المقابل الأصناف المُغلِّفة wrapper classes، التي تعرَّضنا لها بمقال مفهوم المصفوفات الديناميكية ArrayLists في جافا أن تتجاوز ذلك القيد إلى حدٍ بعيد.

يقابل كل نوعٍ أساسي صنفًا مُغلّفًا wrapper class، حيث يوجد لدينا مثلًا الصنف Integer للنوع int؛ والصنف Boolean للنوع boolean؛ والصنف Character للنوع char، وهكذا.

يحتوي أي كائنٍ من النوع Integer على قيمةٍ من النوع int، حيث يعمل الكائن ببساطة مثل مغلِّف wrapper لقيمة النوع الأساسي، ويسمح هذا باستخدام النوع الأساسي ضمن سياقاتٍ تتطلّب بالأساس كائناتٍ مثل بنى البيانات المُعمَّمة generic data structures. يُمكِننا على سبيل المثال تخزين قائمة أعدادٍ صحيحة من النوع Integer بمُتغيّرٍ من النوع ArrayList<Integer>‎، وستكون واجهاتٌ، مثل Collection<Integer>‎ و Set<Integer>‎ مُعرَّفة. يُعرِّف الصنف Integer علاوةً على ذلك التوابع التالية:

  • equals()‎.
  • compareTo()‎.
  • toString()‎.

حيث تُنفِّذ ما ينبغي إجراؤه بما يتناسب مع النوع الأساسي المقابل. تنطبق الأمور نفسها على جميع الأصناف المُغلِّفة wrapper classes.

تُجري لغة جافا تحويلًا تلقائيًا بين الأنواع الأساسية primitive types وما يُقابِلها من أنواعٍ مُغلِّفة. يعني ذلك أنه بمجرد إنشاء بنية بياناتٍ مُعمَّمة تحمل كائناتٍ تنتمي إلى إحدى الأصناف المُغلَّفة، فمن الممكن استخدام بنى البيانات كما لو كان بإمكانها حَمل قيمٍ من النوع الأساسي.

إذا كان numbers مُتغيّرًا من النوع Collection<Integer>‎، فبإمكانك كتابة numbers.add(17)‎، أو numbers.remove(42)‎، ولا يُمكِنك حرفيًا إضافة قيمةٍ من النوع الأساسي مثل 17 إلى numbers؛ وإنما تُحوِّل جافا تلك القيمة تلقائيًا إلى كائنٍ مُغلِّف مُقابِل، أي Integer.valueOf(17)‎، ثم تُضيف ذلك الكائن إلى التجميعة. تُؤثِر عملية إنشاء كائنٍ جديدٍ على كلٍ من الوقت والذاكرة المُستهلَكين أثناء العملية، وهو ما ينبغي أن تَضعُه بالحسبان تحديدًا إذا كنت مهتمًا بكفاءة البرنامج. تُعدّ مصفوفةٌ من النوع int عمومًا أكثر كفاءةً من مصفوفةٍ من النوع ArrayList<Integer>‎.

ترجمة -بتصرّف- للقسم Section 1: Generic Programming من فصل Chapter 10: Generic Programming and Collection Classes من كتاب Introduction to Programming Using Java.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...