الأنواع العامّة

أحرف البدل: ? extends (الحدود العليا)

15 دقيقة الدرس 5 من 13

أحرف البدل: ? extends (الحدود العليا)

تعرّفت حتى الآن على كيفية كتابة الأصناف والدوال الجنيسة (generic). لكن في بعض الأحيان لا تحتاج إلى نوع محدّد — بل تريد فقط القراءة من مجموعة جنيسة مع قبول أنواع متعدّدة ومترابطة في آنٍ واحد. أحرف البدل ذات الحد الأعلى (Upper-Bounded Wildcards) تمنحك هذه المرونة بالضبط.

المشكلة: الأنواع الجنيسة ثابتة (Invariant)

من أكثر الأمور إثارة للدهشة في جنيريكس جافا أن List<Double> ليست نوعًا فرعيًا من List<Number>، حتى وإن كان Double نوعًا فرعيًا من Number. تُعرف هذه الخاصية بـالثبات (invariance).

لماذا؟ لو كانت List<Double> مقبولة باعتبارها List<Number>، لأمكن إضافة Integer إليها — وهو ما ينقض أمان الأنواع. تمنع جافا ذلك عند وقت الترجمة.

List<Double> doubles = List.of(1.5, 2.5); // List<Number> numbers = doubles; // خطأ في الترجمة — ثابت (invariant)!

إذن كيف تكتب دالة مساعدة تجمع أي قائمة من الأرقام — أعدادًا صحيحة أو عشرية؟

أحرف البدل ذات الحد الأعلى: ? extends T

يعني حرف البدل ? "نوع مجهول". بإضافة extends T تقيّد هذا النوع المجهول ليكون T أو أيَّ صنف فرعي منه. والناتج — ? extends T — يُسمّى حرف بدل ذو حد أعلى.

public static double sum(List<? extends Number> list) { double total = 0; for (Number n : list) { total += n.doubleValue(); } return total; }

يمكنك الآن استدعاء sum بـList<Integer> أو List<Double> أو حتى List<BigDecimal>:

List<Integer> ints = List.of(1, 2, 3); List<Double> doubles = List.of(1.1, 2.2); List<Float> floats = List.of(0.5f, 1.5f); System.out.println(sum(ints)); // 6.0 System.out.println(sum(doubles)); // 3.3 System.out.println(sum(floats)); // 2.0
التغاير (Covariance): List<? extends Number> متغايرة — فهي تقبل List<Number> وList<Integer> وList<Double> وأي قائمة من نوع فرعي لـNumber. هذا يشبه تغاير المصفوفات العادية (Integer[] هي Object[])، لكن على مستوى الجنيريكس.

دور المنتِج (Producer): اقرأ، لا تكتب

ثمة قيد جوهري: لا يمكنك إضافة عناصر إلى List<? extends T>. المترجم لا يعرف النوع الدقيق — قد تكون List<Integer> أو List<Double> أو أي شيء آخر. كتابة Integer فيما قد يكون List<Double> أمر غير سليم.

public static void tryAdd(List<? extends Number> list) { // list.add(42); // خطأ في الترجمة — لا يمكن الإضافة // list.add(null); // ممنوع أيضًا Number n = list.get(0); // مقبول — القراءة آمنة دائمًا }

يُلخَّص هذا في النصف الأول من قاعدة PECS (Producer Extends, Consumer Super). قائمة ? extends T تُنتج قيمًا من النوع T يمكنك قراءتها؛ ولا تستهلك شيئًا.

قاعدة PECS: إن كنت تقرأ فقط من مجموعة فاستخدم ? extends T. إن كنت تكتب فقط فاستخدم ? super T (الدرس القادم). إن كنت تقرأ وتكتب معًا فاستخدم معاملًا محدّدًا (concrete type parameter).

مثال تطبيقي: نسخ قائمة

تُجسّد الدالة القياسية Collections.copy(dest, src) قاعدة PECS بأناقة. إليك نسخة مبسّطة:

public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (int i = 0; i < src.size(); i++) { dest.set(i, src.get(i)); } }

ركّز على src: إنها List<? extends T> — نقرأ منها فقط، فهي منتِجة. أما dest فهي ? super T — نكتب إليها فقط، فهي مستهلِكة. يمكنك الآن نسخ List<Integer> إلى List<Number>:

List<Number> dest = new ArrayList<>(List.of(0.0, 0.0)); List<Integer> src = List.of(1, 2); copy(dest, src); System.out.println(dest); // [1, 2]

الحدود العليا خارج المجموعات

لا تقتصر أحرف البدل ذات الحد الأعلى على List، بل تعمل مع أي نوع جنيسي. إليك دالة تجد الحد الأقصى في قائمة عناصرها قابلة للمقارنة:

public static <T extends Comparable<T>> T max(List<? extends T> list) { if (list.isEmpty()) throw new IllegalArgumentException("Empty list"); T result = list.get(0); for (T item : list) { if (item.compareTo(result) > 0) result = item; } return result; } System.out.println(max(List.of(3, 1, 4, 1, 5))); // 5 System.out.println(max(List.of("banana", "apple", "cherry"))); // cherry
خلط شائع: List<? extends Number> وList<T extends Number> يبدوان متشابهين لكنهما يتصرّفان بشكل مختلف. نسخة حرف البدل تُستخدَم في موضع الاستدعاء لقبول أنواع متعدّدة. معامل النوع T يُستخدَم حين تحتاج إلى تسمية النوع وإعادة استخدامه (مثلًا لإعادته). اختر معامل النوع حين تهمّك هوية النوع، واختر حرف البدل حين لا تهمّك.

الخلاصة

  • الأنواع الجنيسة ثابتة (invariant): List<Double> ليست List<Number>.
  • ? extends T يجعل النوع متغايرًا (covariant) — يقبل T وجميع أنواعه الفرعية.
  • يمكنك القراءة من مجموعة ? extends T لكن لا يمكنك الكتابة إليها أبدًا.
  • هذا هو الجزء Producer Extends من قاعدة PECS.
  • مثالية لدوال المساعدة التي تعالج مجموعة دون تعديلها.