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

كتابة أصناف وتوابع معممة في جافا


رضوى العربي

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

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

أصناف معممة بسيطة

لنبدأ بمثالٍ بسيطٍ لتوضيح أهمية البرمجة المُعمَّمة. ذَكَرنا في مقال القوائم lists والأطقم sets في جافا أنه من الأسهل تنفيذ الرتل queue باستخدام قائمةٍ مترابطةٍ من النوع LinkedList. وللتأكُّد من أننا سنُطبِق فقط إحدى عمليات الأرتال، مثل enqueue و dequeue و isEmpty على القائمة، يُمكِننا إنشاء صنفٍ جديدٍ وإضافة مُتغيِّر نسخة خاص private instance variable إليه، يُمثِّل القائمة المترابطة. تُعرِّف الشيفرة التالية مثلًا صنفًا لتمثيل رتلٍ من السلاسل النصية من النوع String:

class QueueOfStrings {
   private LinkedList<String> items = new LinkedList<>();
   public void enqueue(String item) {
      items.addLast(item);
   }
   public String dequeue() {
      return items.removeFirst();
   }
   public boolean isEmpty() {
      return (items.size() == 0);
   }
}

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

توضِح الشيفرة التالية الصياغة المُستخدَمة لكتابة صنفٍ مُعمَّمٍ generic، وهي في الواقع بسيطةٌ للغاية؛ حيث ينبغي فقط استبدال معامل نوع type parameter، مثل T بالنوع String، كما ينبغي بالطبع إضافة معامل النوع إلى اسم الصنف على النحو التالي:

class Queue<T> {
   private LinkedList<T> items = new LinkedList<>();
   public void enqueue(T item) {
      items.addLast(item);
   }
   public T dequeue() {
      return items.removeFirst();
   }
   public boolean isEmpty() {
      return (items.size() == 0);
   }
}

لاحظ أننا نُشير إلى معامل النوع T ضمن تعريف الصنف كما لو كان اسم نوعٍ عادي؛ حيث اسُتخدِم للتصريح عن النوع المُعاد من التابع dequeue، وتخصيص نوع المُعامِل الصوري formal parameter ‏item الذي يَستقبِله التابع enqueue، واسُتخدم كذلك مثل معامل نوع type parameter فعليّ باسم النوع LinkedList<T>‎. والآن، بعد أن عرَّفنا صنفًا ذا معاملاتٍ غير مُحدَّدة النوع parameterized types، يُمكِننا استخدام أسماء أصنافٍ، مثل Queue<String>‎، أو Queue<Integer>‎، أو Queue<Color>‎ بنفس الكيفية التي نَستخدِم بها الأصناف المُعمَّمة المبنية مُسبقًا built-in، مثل LinkedList و HashSet.

يُمكِنك استخدام أي اسمٍ وليس بالضرورة T لمعامل النوع type parameter أثناء تعريف الصنف المُعمَّم، كما تفعل مع المعاملات الصورية formal parameters، وهو ما نُعرِِّفه أثناء كتابة التابع لنتمكَّن من الإشارة إليه بالتابع؛ أما المعامل الفعلي actual parameter فهو القيمة الفعلية الممررة للتابع بتعريفات البرامج الفرعية subroutines. عندما تَستخدِم ذلك الصنف للتصريح عن مُتغيّرٍٍ، أو إنشاء كائنٍ object، فسيَحلّ اسم النوع الفعليّ الذي تُخصِّصه محلّ الاسم الموجود تعريف definition الصنف. إذا كنت تُفضِّل أن يكون اسم معامل النوع type parameter ذا معنىً، يُمكِنك أن تُعرِّف الصنف Queue على النحو التالي مثلًا:

class Queue<ItemType> {
   private LinkedList<ItemType> items = new LinkedList<>();
   public void enqueue(ItemType item) {
      items.addLast(item);
   }
   public ItemType dequeue() {
      return items.removeFirst();
   }
   public boolean isEmpty() {
      return (items.size() == 0);
   }
}

لا يؤثر اختيارك للاسم "T"، أو للاسم "ItemType" على تعريف الصنف Queue أو على الطريقة التي يَعمَل بها نهائيًا.

يُمكِنك تعريف الواجهات المُعمَّمة generic interfaces بنفس الكيفية، وتستطيع كذلك تعريف أصنافٍ أو واجهاتٍ interfaces مُعمَّمة بمُعاملي نوعٍ أو أكثر، مثل الواجهة القياسية Map<K,V>‎، بنفس الكيفية والسهولة. يتَضمَّن تعريف الصنف Pair بالشيفرة التالية على سبيل المثال كائنين objects من أي نوع:

class Pair<T,S> {
   public T first;
   public S second;
   public Pair( T a, S b ) {  // بانٍ
      first = a;
      second = b;
   }
}

والآن، تَستطيع التصريح عن مُتغيِّراتٍ من النوع Pair المُعرَّف بالأعلى، أو إنشاء كائناتٍ منه على النحو التالي:

Pair<String,Color> colorName = new Pair<>("Red", Color.RED);
Pair<Double,Double> coordinates = new Pair<>(17.3,42.8);

ملاحظة: اِستخدَمنا الاسم "Pair" بتعريف الباني constructor بالأعلى دون تخصيص معاملات نوع type parameters. ربما توقَّعت أن نَستخدِم شيئًا، مثل "Pair‎"، ولكن في الحقيقة، اسم الصنف هو "Pair" وليس "Pair‎"؛ فليس الاسمان "T" و"S" ضمن تعريف الصنف أكثر من مجرد أسماءٍ تُشير للأنواع الفعلية المُخصَّصة. في العموم، لا تُخصَّص أبدًا معاملات الأنواع type parameters بأسماء التوابع methods والبواني constructors، وإنما فقط بأسماء الأصناف والواجهات. ,s>,s>

توابع معممة بسيطة

تُوفِّر جافا أيضًا ما يُعرَف باسم التوابع المُعمَّمة generic methods، ويُعدّ التابع Collections.sort()‎ مثالًا عليها؛ حيث يُمكِنه أن يُرتِّب تجميعات كائناتٍ من أي نوع. لنَكْتُب الآن تابًعا غير مُعمَّم non-generic يَستقبِل سلسلةً نصيةً، ثم يَعُدّ عدد مرات حدوث تلك السلسلة ضمن مصفوفة من السلاسل النصية. ألقِ نظرةً على الشيفرة التالية:

// 1
public static int countOccurrences(String[] list, String itemToCount) {
   int count = 0;
   if (itemToCount == null) {
      for ( String listItem : list )
         if (listItem == null)
            count++;
   }
   else {
      for ( String listItem : list )
         if (itemToCount.equals(listItem))
            count++;
   }
   return count;
}

[1] يُعيد عدد مرات حدوث itemToCount بالقائمة. يُستخدَم التابع itemToCount.equals()‎ لاختبار فيما إذا كان كل عنصرٍ ضمن القائمة يُساوِي itemToCount، إلّا إذا كان itemToCount فارغًا.

أصبح لدينا الآن شيفرةٌ تَعمَل فقط مع النوع String، ويُمكِننا أن نتخيل كتابة نفس الشيفرة تقريبًا مرةً بعد أخرى إذا أردنا اِستخدَامها مع أنواعٍ أخرى من الكائنات. وفي المقابل، إذا كتبنا التابع بصيغةٍ مُعمَّمة، فيُمكِننا أن نُعرِّفه مرةً واحدةً فقط، ونَستخدِمه مع كائناتٍ منتميةٍ لأي نوع، حيث سنستبدل فقط اسم معامل النوع، وليكن "T"، باسم النوع String ضمن تعريف التابع. إذا اكتفينا بهذا التغيير، سيَظّن المُصرِّف أن الاسم "T" يُمثِل اسم النوع الفعليّ، ولهذا يجب أن نخبره بأن 'T' هي معامل نوعٍ type parameter لا أكثر. بالنسبة للأصناف المُعمَّمة، كنا قد أضفنا '' بتعريفه، مثل class Queue<T> { ...‎؛ أما بالنسبة للتوابع المُعمَّمة generic methods، فينبغي إضافة "" قبل اسم النوع المُعاد return type من التابع على النحو التالي:

public static <T> int countOccurrences(T[] list, T itemToCount) {
   int count = 0;
   if (itemToCount == null) {
      for ( T listItem : list )
         if (listItem == null)
            count++;
   }
   else {
      for ( T listItem : list )
         if (itemToCount.equals(listItem))
            count++;
   }
   return count;
}   

يُشير '' إلى كون التابع مُعمَّمًا generic، كما يُخصِّص اسم معامل النوع type parameter الذي سنَستخدِمه داخل التعريف definition. يُمكِنك بالطبع أن تختار أي اسمٍ آخر غير "T" اسمًا لمعامل النوع. قد يبدو موضع "" غريبًا بعض الشيء، ولكن في جميع الأحوال، كان من الضروري وضعه بموضعٍ ما، وكان هذا الموضع هو الذي اتفق عليه مُصمِّمي لغة جافا.

بعد أن عرَّفنا التابع المُعمَّم بالأعلى، يُمكِننا الآن تطبيقه على أي كائناتٍ من أي نوع. إذا كان wordList مُتغيرًا من النوع String[]‎ مثلًا، وكان word مُتغيِّرًا من النوع String، فسيَعُدّ الاستدعاء التالي عدد مرات حدوث word بالمصفوفة wordList:

int ct = countOccurrences( wordList, word );

وبالمثل، إذا كان palette مُتغيِّرًا من النوع Color[]‎، وكان color مُتغيِّرًا من النوع Color، فسيَعُدّ الاستدعاء التالي عدد مرات حدوث color بالمصفوفة palette:

int ct = countOccurrences( palette, color );

وبالمثل أيضًا، إذا كان numbers مُتغيِّرًا من النوع Integer[]‎، فسيَعُدّ الاستدعاء التالي عدد مرات حدوث العدد 17 بالمصفوفة numbers:

int ct = countOccurrences( numbers, 17 );

يَستخدِم المثال الأخير خاصية التغليف التلقائي autoboxing؛ أي يُحوِّل الحاسوب العدد 17 تلقائيًا إلى قيمةٍ من النوع Integer.

اقتباس

ملاحظة: بما أن البرمجة المُعمَّمة generic programming بلغة جافا تُطبَّق فقط على الكائنات objects، فلا يُمكِننا استخدام countOccurrences لعدّ عدد مرات حدوث عددٍ مثل 17 بمصفوفةٍ من النوع int[]‎.

يُمكِن أن يمتلك تابعٌ مُعمَّمٌ generic method معامل نوعٍ واحدٍ أو أكثر، مثل T بالتابع countOccurrences. عندما نَستدعِي تابعًا مُعمَّمًا، مثل countOccurrences(wordlist, word)‎، فإننا لا نذكُر صراحةً النوع الفعليّ الذي استُبدل بمعامل النوع؛ حيث يَستنتجه المُصرِّف من نوع المُعاملات الفعليّة المُمرَّرة بتعليمة الاستدعاء. يُدرِك المُصرِّف تلقائيًا في تعليمة الاستدعاء countOccurrences(wordlist, word)‎ مثلًا أن عليه استبدال النوع String بمعامل النوع T ضمن التعريف؛ لأن wordlist من النوع String[]‎. يختلف ذلك عن الأصناف المُعمَّمة generic classes، مثل Queue<String>‎، حيث ينبغي أن نُخصِّص اسم معامل النوع المطلوب صراحةً.

يُعالِج التابع countOccurrences مصفوفةً؛ وبالتالي يُمكِننا كتابة تابعٍ مشابهٍ لعدّ عدد مرات حدوث كائنٍ ضمن أي نوعٍ من التجميعات على النحو التالي:

public static <T> int countOccurrences(Collection<T> collection, T itemToCount) {
   int count = 0;
   if (itemToCount == null) {
      for ( T item : collection )
         if (item == null)
            count++;
   }
   else {
      for ( T item : collection )
         if (itemToCount.equals(item))
            count++;
   }
   return count;
}

بما أن Collection<T>‎ مُعمَّم النوع فإن التابع المذكور بالأعلى مُصمّمٌ ليكون عامًا جدًا؛ فيُمكِنه مثلًا العمل مع قائمةٍ من النوع ArrayList تتضمِّن كائناتٍ من النوع Integer؛ أو مع طقمٍ من النوع TreeSet يتضمِّن كائناتٍ من النوع String؛ أو مع قائمةٍ مترابطةٍ من النوع LinkedList تتضمِّن كائناتٍ من النوع Button.

أنواع البدل Wildcard Types

في الحقيقة، هناك نوعٌ من التقييد بنوعية الأصناف والتوابع المُعمَّمة التي تعاملنا معها حتى الآن؛ حيث يُمكِن لأي معامل نوع type parameter، أي ما نُسميه T عادةً، أن ينتمي لأي نوعٍ على الإطلاق، وهو أمرٌ جيدٌ في أغلب الأحوال، ولكنه يَعنِي في نفس الوقت أن الأشياء التي يُمكِننا أن نُطبِّقها على T هي فقط تلك الأشياء التي بإمكاننا أن نُجرِيها على كل الأنواع؛ وأن الأشياء التي يُمكِننا أن نُطبِقها على كائناتٍ من النوع T هي فقط تلك الأشياء التي بإمكاننا أن نُجرِيها على كل الكائنات؛ بمعنى أنه لا يُمكِنك مثلًا كتابة تابعٍ مُعمَّمٍ يُوازن بين الكائنات باستخدام التابع compareTo()‎، لأنه غير مُعرَّفٍ بكل الكائنات، وإنما بالواجهة Comparable.

بناءً على ذلك، نحتاج أن نُوضِح للحاسوب بطريقةٍ ما أنه يمكن تطبيق صَنْفٍ أو تابعٍ مُعمَّمٍ معين فقط على كائنٍ من النوع Comparable وليس على أي كائنٍ عشوائي، وهو ما سيُمكِّننا من استخدام تابعٍ، مثل compareTo()‎ بتعريف صنفٍ أو تابعٍ مُعمَّم. تُوفِّر جافا قاعدتي صياغة syntax لتخصيص هذا النوع من التقييد بالأنواع المُستخدَمة للبرمجة المُعمَّمة generic programming.

تعتمد الطريقة الأولى على ما يُعرَف باسم معاملات الأنواع المُقيَّدة bounded، والتي تُستخدَم مثل معاملات نوعٍ صورية formal ضمن تعريفات الأصناف والتوابع المُعمَّمة؛ حيث يَحِلّ معامل النوع المُقيَّد bounded محل معامل النوع البسيط، مثل T، عند تعرّيف صنفٍ، مثل class GenericClass<T> ...‎، أو تعريف تابعٍ، مثل public static <T> void genericMethod(...‎.

تعتمد الطريقة الثانية على ما يُعرَف باسم أنواع البدل wildcard types، والتي تُستخدَم مثل معاملات نوع أثناء التصريح عن كُلٍ من المُتْغيِّرات والمعاملات الصُوريّة formal parameters ضمن تعريفات التوابع؛ حيث يَحِلّ نوع بدلٍ محلّ معامل نوعٍ، مثل String بتعليمة تصريح، مثل List<String> list;‎، أو بقائمة معاملات صوريّة، مثل void concat(Collection<String> c)‎. سنناقش أولًا أنواع البدل wildcard types، ثم سننتقل إلى مناقشة الأنواع المُقيَّدة bounded types لاحقًا.

سنبدأ بمثالٍ بسيط يُوضِح الغرض من أنواع البدل. لنفترض أن الصنف Shape يُعرِّف التابع public void draw()‎، وأن الصنفين Rect و Oval مُشتقان منه، وأننا نريد كتابة تابع ليَرسِم جميع الأشكال المُخزَّنة ضمن تجميعة كائناتٍ من النوع Shape. يُمكِننا تعريف التابع على النحو التالي:

public static void drawAll(Collection<Shape> shapes) {
   for ( Shape s : shapes )
      s.draw();
}

سيَعمَل التابع المُعرَّف بالأعلى جيدًا إذا طبقناه على مُتغيّرٍ من النوع Collection<Shape>‎، أو من النوع ArrayList<Shape>‎، أو أي صنف تجميعةٍ collection آخر بمعامل نوع Shape. والآن، لنفترض أن لدينا قائمة list كائناتٍ من النوع Rect مُخزَّنةً بمُتغيّرٍ اسمه rectangles من النوع Collection<Rect>‎. نظرًا لأن كائنات الصنف Rect هي بالنهاية من النوع Shape، ربما تظن أن بإمكانك إجراء الاستدعاء drawAll(rectangles)‎، ولكن هذا للأسف غير صحيح؛ حيث تختلق تجميعة كائناتٍ من النوع Rect عن تجميعة كائناتٍ من النوع Shape، وبالتالي لا يُمكِنك اسنادد المُتغيِّر rectangles للمعامل الصوري shapes.

يُمكِننا تجاوز تلك المشكلة بتعديل معامل النوع المُستخدَم بالتصريح عن shapes من النوع Shape إلى نوع البدل ‎? extends Shape على النحو التالي:

public static void drawAll(Collection<? extends Shape> shapes) {
   for ( Shape s : shapes )
      s.draw();
}

يَعنِي نوع البدل ‎? extends Shape النوع Shape أو الأنواع الفرعية subclass منه. إذا صرَّحنا عن المعامل الصوري shapes على أنه من النوع Collection<? extends Shape>‎، يُمكِننا عندها أن نُمرِّر مُعاملًا فعليًا من النوع type Collection<Rect>‎ للتابع drawAll؛ لأن Rect صنفٌ مُشتَقٌ من الصنف Shape، أي أنه متوافقٌ مع نوع البدل wildcard type المطلوب. بإمكاننا أيضًا تمرير معاملاتٍ فعلية من النوع ArrayList<Rect>‎، أو Set<Oval>‎، أو List<Oval>‎، وما يزال أيضًا بإمكاننا تمرير مُتغيِّراتٍ من النوع Collection<Shape>‎ و ArrayList<Shape>‎؛ لأن الصنف Shape متوافقٌ مع ‎? extends Shape. بفضل أنواع البدل wildcard type، نكون قد استفدنا من التابع المُعرَّف بأقصى حدٍ ممكن.

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

static void addOval(List<Shape> shapes, Oval oval) {
   shapes.add(oval);
}

إذا كانت rectangles من النوع List<Rect>‎، فإننا لا نستطيع استدعاء addOval(rectangles,oval)‎؛ نتيجةً للقاعدة التي تنص على عدم عدّ قائمةٍ من النوع Rect قائمةً من النوع Shape. إذا أسقطنا تلك القاعدة، وتمكَّنا من استدعاء addOval(rectangles,oval)‎؛ نكون قد أضفنا كائنًا من النوع Oval إلى قائمة كائناتٍ من النوع Rect، وهو أمرٌ سيءٌ للغاية. بما أن الصنف Oval غير مُشتقَّ من Rect، لا تُعدّ كائنات الصنف Oval من النوع Rect، وعليه لا ينبغي أبدًا لقائمة كائناتٍ من النوع Rect أن تتضمَّن كائنًا من النوع Oval؛ ولهذا لا يكون للاستدعاء addOval(rectangles,oval)‎ أيّ معنى، ولا ينبغي السماح به.

لنناقش الآن مثالًا آخر، بحيث تتضمَّن الواجهة Collection<T>‎ التابع addAll()‎، الذي كنا قد وصفناه بمقال مفهوم البرمجة المعممة Generic Programming، وذَكَرَنا أنه من أجل تجميعةٍ coll من النوع Collection<T>‎، سيضيف الاستدعاء coll.addAll(coll2)‎ جميع كائنات التجميعة coll2 إلى coll. يُمكِن أن يُمثِّل المعامل coll2 أي تجميعةٍ من النوع Collection<T>‎، ولكنه قد يَكون أعمُ من ذلك. إذا كان T صنفًا على سبيل المثال، وكان S صنفًا فرعيًا subclass من T، فيُمكِن للمتغيّرcoll2 أن يكون من النوع Collection<S>‎، وهو أمرٌ منطقي؛ لأن أي كائنٍ من النوع S هو بالضرورة كائنٌ من النوع T، ويُمكِن إضافته إلى coll.

إذا فكرت للحظة، ستَجِد أن هذا الوصف يُعدّ بطريقةٍ أو بأخرى اِستخدَامًا لأنواع البدل wildcard types؛ فنحن لا نريد للمُتغيِّر coll2 أن يكون تجميعة كائناتٍ من النوع T، وإنما نُريد أن نَسمَح له بأن يكون أي تجميعة كائنات، طالما كانت تنتمي لصَنْفٍ مُشتَقٍ من T. لنرى كيف يُمكِننا إضافة التابع addAll()‎ إلى الصنف المُعمَّم Queue الذي عرَّفناه ببداية هذا المقال، حتى تُصبِح الأمور أكثر وضوحًا:

class Queue<T> {
   private LinkedList<T> items = new LinkedList<T>();
   public void enqueue(T item) {
      items.addLast(item);
   }
   public T dequeue() {
      return items.removeFirst();
   }
   public boolean isEmpty() {
      return (items.size() == 0);
   }
   public void addAll(Collection<? extends T> collection) {
       // أضِف جميع عناصر التجميعة إلى نهاية الرتل
      for ( T item : collection ) 
         enqueue(item);
   }
}

يُعدّ T بهذا المثال معامل نوعٍ type parameter ضِمْن تعريف صنفٍ مُعمَّم، ويُمكِنك أن ترى أننا اِستخدَمنا نوع بدل wildcard class داخل الصنف المُعمَّم generic class. تُستَخدَم T داخل التعريف كما لو كانت نوعًا مُخصَّصًا، ولكنه غير معروف بعد. يَعنِي نوع البدل ‎? extends T أي نوعٍ يُساوِي النوع المُخصَّص T أو يتوسَّع extend منه، وبالتالي، عندما نُنشِئ رتلًا queue من النوع Queue<Shape>‎، ستُشير T إلى Shape؛ في حين سيُشير نوع البدل ‎? extends T ضمن تعريف الصنف إلى ‎? extends Shape. يضمَن لنا ذلك إمكانية تطبيق التابع addAll على تجميعة كائناتٍ من النوع Rect، أو Oval بالإضافة إلى Shape بالتأكيد.

تَستخدِم حلقة التكرار for-each الموجودة بتعريف التابع addAll المُتغيّر item من النوع T للمرور عبر التجميعة collection، إذ يُمكِن للتجميعة الآن أن تكون من النوع Collection<S>‎، حيث S صنفٌ مُشتَقٌ من T. نظرًا لأن المُتغيّر item من النوع T، وليس النوع S، هل يُشكِّل ذلك مشكلة؟ لا؛ فطالما كان S صنفًا فرعيًا من T، فإننا نستطيع اسناد أي قيمةٍ من النوع S إلى مُتغيِّر من النوع T؛ حيث يضمَن نوع البدل wildcard type المُستخدَم أن يَعمَل كل شيءٍ على النحو الصحيح.

يُضيف التابع addAll جميع العناصر الموجودة ضمن تجميعةٍ معينة إلى رتل. لنفترض الآن أننا نريد إجراء تلك العملية بصورةٍ معكوسة؛ أي أن نُضيف جميع العناصر الموجودة ضمن رتلٍ إلى تجميعةٍ معينة. إذا عرَّفنا تابع نسخة instance method على النحو التالي، فسيَعمَل فقط مع التجميعات التي يساوي نوعها الأساسي base type النوع T تحديدًا:

public void addAllTo(Collection<T> collection)

وهذا أمرٌ مُقيِّدٌ للغاية، لذلك ربما من الأفضل استخدام نوع بدل wildcard type، حيث تَستخِدم الشيفرة التالية نوع البدل ‎? extends T، ولكنه لن يَعمل أيضًا:

public void addAllTo(Collection<? extends T> collection) {

    // احذف جميع العناصر الموجودة حاليًا بالرتل، وأضِفها إلى التجميعة
   while ( ! isEmpty() ) {
      T item = dequeue();  // احذف عنصرًا من الرتل
      collection.add( item );  //‫ أضفه إلى التجميعة، غير صالح !
   }
}

تَكْمُن المشكلة في عدم التمكُّن من إضافة عنصرٍ من النوع T إلى تجميعةٍ قد يَكون بإمكانها حَمْل عناصرٍ تنتمي فقط إلى صنفٍ فرعي subclass من T، مثل S، حيث من الضروري لعنصرٍ من النوع T أن يكون من النوع S. لنفترض مثلًا أن لدينا رتلًا queue من النوع Queue<Shape>‎، فلن يكون لعملية إضافة عناصر ذلك الرتل إلى تجميعةٍ من النوع Collection<Rect>‎ أيُّ معنى؛ حيث لا تنتمي كل كائنات النوع Shape إلى الصنف Rect بالضرورة. في المقابل، إذا كان لدينا رتلًا من النوع Queue<Rect>‎، تُصبِح عملية إضافة عناصر الرتل إلى تجميعةٍ من النوع Collection<Shape>‎، أو حتى من النوع Collection<S>‎ ذات معنى، حيث S صنفٌ أعلى superclass من Rect.

سنحتاج إذًا إلى نوعٍ آخرٍ من أنواع البدل للتعبير عن تلك العلاقة، وهو ‎? super T؛ الذي يَعنِي T ذاته أو أي صنفٍ أعلى superclass من T. على سبيل المثال، يَتماشَى النوع Collection<? super Rect>‎ مع أنواعٍ، مثل Collection<Shape>‎ و ArrayList<Object>‎ و Set<Rect>‎. في الواقع، يُمثِل نوع البدل ذاك ما نحتاجه تمامًا بالتابع addAllTo، وبإجراء هذا التغيير، سيُصبِح الصنف المُعمَّم Queue جاهزًا على النحو التالي:

class Queue<T> {
   private LinkedList<T> items = new LinkedList<T>();
   public void enqueue(T item) {
      items.addLast(item);
   }
   public T dequeue() {
      return items.removeFirst();
   }
   public boolean isEmpty() {
      return (items.size() == 0);
   }
   public void addAll(Collection<? extends T> collection) {
       // أضِف جميع عناصر التجميعة إلى نهاية الرتل
      for ( T item : collection ) 
         enqueue(item);
   }
   public void addAllTo(Collection<? super T> collection) {
       // احذف جميع العناصر الموجودة حاليًا بالرتل، وضِفها إلى التجميعة
      while ( ! isEmpty() ) {
         T item = dequeue();  // اِحذف عنصر من الرتل
         collection.add( item );  // أضف العنصر إلى التجميعة
      }
   }
}

قد يشير T باسم نوع بدلٍ wildcard type معينٍ، مثل ‎? extends T، إلى واجهةٍ interface بدلًا من صنفٍ. لاحِظ أننا سنستمر باستخدام كلمة "extends" وليس "implements" باسم نوع البدل حتى لو كان T واجهةً بالأساس. لنفحص مثالًا على ذلك: كنا قد تعرَّضنا للواجهة Runnable، والتي تُعرِّف التابع public void run()‎. يُمكِننا إذًا أن نُطبِق التابع method التالي على جميع تجميعة كائناتٍ من النوع Runnable باستدعاء التابع run()‎ المُعرَّف بكل كائنٍ منها:

public static runAll( Collection<? extends Runnable> runnables ) {
   for ( Runnable runnable : runnables ) {
      runnable.run();
   }
}

تُستخدَم أنواع البدل Wildcard types فقط بمثابة معاملات أنواع type parameters بالأنواع ذات المعاملات غير مُحدَّدة النوع parameterized types، مثل Collection<? extends Runnable>‎، وستَجدِها غالبًا ضمن قوائم المعاملات الصوريّة؛ حيث تُستخدَم للتصريح declaration عن نوع معاملٍ صوريٍ formal parameter معين. كما ستَجِدها مُستخدَمةٌ أيضًا بعدة أماكنٍ أخرى، مثل تعليمات التصريح عن المتغيرات variable declaration.

ملاحظة أخيرة: يُعدّ <?> نوع بدلٍ مكافئٍ تمامًا لنوع البدل <‎? extends Object>، ويُقصَد منه مطابقة أي نوعٍ مُحتمَل. تُصرِّح الواجهة المُعمَّمة Collection<T>‎ على سبيل المثال عن التابع removeAll على النحو التالي:

public boolean removeAll( Collection<?> c ) { ...

والذي يَعني أن التابع removeAll يُمكِن تطبيقه على أي تجميعة كائناتٍ تنتمي بدورها لأي نوع.

الأنواع المقيدة Bounded Types

لا تحلّ أنواع البدل wildcard types جميع مشاكلنا، وإنما تَسمَح لنا بتعميم تعريفات التوابع؛ بحيث نتمكّن من اِستخدَامها مع تجميعات كائناتٍ من أنواعٍ مختلفةٍ، بدلًا من تقييدها بنوعٍ واحدٍ فقط؛ بينما لا تَسمَح لنا بتقييد أنواع معاملات الأنواع type parameters المسموح بها ضمن تعريفات الأصناف والتوابع المُعمَّمة generic، وهذا هو الغرض من وجود الأنواع المُقيَّدة bounded types.

لنبدأ بمثالٍ صغير وإن كان غير واقعيٍ بعض الشيء. بفرض أننا نريد إنشاء مجموعةٍ من المكوِّنات لواجهة مُستخدِم رسومية GUI باستخدام صنفٍ مُعمّم اسمه ControlGroup؛ فيُمثِل النوع ControlGroup<Button>‎ مثلًا مجموعةً من الأزرار؛ بينما يُمثِل ControlGroup<Slider>‎ مجموعةً من المزالج. سيتضمَّن الصنف توابعًا لتطبيق بعض العمليات المُحدَّدة على جميع مُكوِّنات المجموعة بنفس الوقت. فمثلًا، قد نُعرِّف تابع النسخة instance method التالي:

public void disableAll() {
   .
   .  // 1
   .
}

حيث تعني [1]: ‫‫اِستدعِ c.setDisable(true)‎ لكل مكوِّن c ضمن المجموعة.

توجد مشكلةٌ في أن التابع setDisable()‎ مُعرَّفٌ فقط للكائنات من النوع Control، ولا يَعمل لجميع أنواع الكائنات؛ أي لا يُعدّ السماح بوجود أنواع، مثل ControlGroup<String>‎ و ControlGroup<Integer>‎ أمرًا منطقيًا؛ لعدم تضمُّن السلاسل النصية من النوع String والأعداد الصحيحة من النوع Integer للتابع setDisable()‎. نحتاج إذًا إلى طريقةٍ لتقييد معامل النوع T بالنوع ControlGroup<T>‎، بحيث نَسمَح لقيم المعاملات الفعلية actual parameters بالانتماء فقط إما إلى الصنف Control أو إلى الأصناف الفرعية subclasses المُشتقَّة منه، وهو ما يُمكِننا تطبيقه باستخدام النوع المُقيَّد T extends Control بدلًا من النوع T ضمن تعريف الصنف على النحو التالي:

public class ControlGroup<T extends Control> {
   private ArrayList<T> components; // من أجل ترتيب المكونات في هذه المجموعة
   public void disableAll( ) {
      for ( Control c : components )
         if (c != null)
            c.setDisable(true);
      }
   }
   public void enableAll( ) {
      for ( Control c : components )
         if (c != null)
            c.setDisable(false);
      }
   }
   public void add( T c ) {  // ‫أضِف قيمة c من النوع T إلى المجموعة
      components.add(c);
   }
   .
   .  // توابع وبناء إضافية
   .
}

يمنعنا التقييد extends Control الذي فرضناه على معامل النوع T من إنشاء أنواعٍ ذات مُعاملات غير مُحدَّدة النوع، مثل ControlGroup<String>‎ و ControlGroup<Integer>‎؛ لأنه من الضروري لمعامل النوع type parameter الفعلي الذي سيحلّ محل T أن ينتمي للنوع Control نفسه أو إلى صنفٍ فرعي منه. باِستخدامنا لهذا التقييد، أصبح المُصرِّف على علمٍ بأن مُكوِّنات المجموعة تنتمي إلى النوع Control، وُتصبِح العملية c.setDisable()‎ مُعرَّفةً لأي مُكوِّنٍ c ضمن المجموعة.

عندما نَستخدِم معامل نوعٍ مقُيَّد، مثل T extends SomeType، فإننا نَعنِي في العموم النوع T الذي إما أن يُساوِي SomeType، أو يُساوِي صنفًا فرعيًا من SomeType، ويترتَّب على ذلك عدُّ أي كائنٍ من النوع T من النوع SomeType أيضًا، وتُصبِح أي عمليةٍ مُعرَّفةٍ لكائنات النوع SomeType مُعرَّفةً أيضًا لكائنات النوع T. لا ينبغي أن يكون SomeType اسمًا لصنفٍ بالضرورة؛ حيث يُمكِن أن يكون أي اسمٍ يُمثِل النوع الفعليّ للكائن، فقد يَكون واجهة interface مثلًا، أو حتى نوعًا ذا مُعاملاتٍ غير مُحدَّدة النوع parameterized type.

في حين تتشابه الأنواع المُقيَّدة bounded types مع أنواع البدل wildcard types، فإنها تُستخدَم بطرائقٍ مختلفة؛ حيث يُستخدَم النوع المُقيَّد عادةً مثل معامل نوعٍ صوري formal type parameter بتعريفٍ مُعمَّم generic لتابعٍ، أو صنفٍ، أو واجهة؛ بينما يُستخدَم نوع البدل wildcard type كثيرًا للتصريح عن نوع معاملٍ صوريٍ معينٍ ضمن تابع، ولا يُمكِن أن يُستخدَم مثل معامل نوعٍ صوري. إضافةً إلى ذلك، لا يُمكِن لمعاملات الأنواع المُقيَّدة bounded type parameters استخدام "super" نهائيًا، وإنما تَستخدِم "extends" فقط بخلاف أنواع البدل wildcard types.

تُستخدَم معاملات الأنواع المُقيَّدة أثناء التصريح عن التوابع المُعمَّمة. على سبيل المثال، بدلًا من استخدام الصنف المُعمَّم ControlGroup، يُمكِننا كتابة التابع الساكن static method المُعمّم التالي لتعطيل أي تجميعة كائناتٍ من النوع Control:

public static <T extends Control> void disableAll(Collection<T> comps) {
   for ( Control c : comps )
      if (c != null)
         c.setDisable(true);
}

نتيجةً لاستخدامنا معامل النوع الصوري <T extends Control>، لا يُمكِننا الآن استدعاء التابع إلا مع تجميعةٍ نوعها الأساسي base type يُساوِي Control، أو أي صنفٍ فرعيٍ مشتق منه، مثل Button أو Slider.

لاحِظ أننا لا نحتاج بالضرورة إلى معامل نوعٍ مُعمَّم generic type parameter بتلك الحالة، حيث نستطيع أيضًا كتابته باستخدام نوع بدل wildcard type على النحو التالي:

public static void disableAll(Collection<? extends Control> comps) {
   for ( Control c : comps )
      if (c != null)
         c.setDisable(true);
}

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

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

static void sortedInsert(List<String> sortedList, String newItem) {
   ListIterator<String> iter = sortedList.listIterator();
   while (iter.hasNext()) {
      String item = iter.next();
      if (newItem.compareTo(item) <= 0) {
         iter.previous();
         break;
      } 
   }
   iter.add(newItem);
}

يَعمل هذا التابع جيدًا مع قائمةٍ من السلاسل النصية، ولكن سيكون من الأفضل لو تمكَّنا من كتابة تابعٍ مُعمَّم generic method يُمكِن تطبيقه على قوائمٍ من أنواعٍ مختلفةٍ من الكائنات. تكمن المشكلة في افتراض الشيفرة بأن التابع compareTo()‎ مُعرَّفٌ بالكائنات الموجودة ضمن القائمة، وبالتالي يَعمَل التابع فقط مع القوائم التي تتضمَّن كائناتٍ تُنفِّذ الواجهة Comparable، ولا يُمكِننا إذًا استخدام نوع بدل wildcard type لفرض هذا التقييد. لنفترض حتى أننا حاولنا فعل ذلك بكتابة List<? extends Comparable>‎ بدلًا من List<String>‎:

static void sortedInsert(List<? extends Comparable> sortedList, ???? newItem) {
   ListIterator<????> iter = sortedList.listIterator();
   ...

سنقع بمشكلةٍ على الفور، لأننا لا نملُك اسمًا للنوع غير المعروف الذي يُمثِّله نوع البدل، ونحن بنفس الوقت بحاجةٍ إلى ذلك الاسم؛ لأنه من الضروري لكُلٍ من newItem و iter أن يكونا من نفس نوع عناصر القائمة. يُمكِننا لحسن الحظ حل تلك المشكلة إذا كتبنا تابعًا مُعمَّمًا بمعامل نوع مُقيَّد bounded type parameter؛ حيث سيتوفَّر لنا اسمٌ للنوع غير المعروف بهذه الطريقة. ألقِ نظرةً على شيفرة التابع المُعمَّم:

static <T extends Comparable> void sortedInsert(List<T> sortedList, T newItem) {
   ListIterator<T> iter = sortedList.listIterator();
   while (iter.hasNext()) {
      T item = iter.next();
      if (newItem.compareTo(item) <= 0) {
         iter.previous();
         break;
      } 
   }
   iter.add(newItem);
}

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

static <T extends Comparable<T>> void sortedInsert(List<T> sortedList, ...

ترجمة -بتصرّف- للقسم Section 5: Writing Generic Classes and Methods من فصل 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.


×
×
  • أضف...