تُشير البرمجة المُعمَّمة 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.