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

أحرف البدل: ? super (الحدود الدنيا)

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

أحرف البدل: ? super (الحدود الدنيا)

في الدرس السابق تعرّفت على ? extends T الذي يتيح لك القراءة من مجموعة بأمان. يتناول هذا الدرس نظيره المعاكس: ? super T، وهو حرف البدل ذو الحدّ الأدنى. بينما يُستخدم ? extends للمنتجين (العناصر التي تُعطيك البيانات)، يُستخدم ? super للمستهلكين (consumers) أي العناصر التي تقبل البيانات التي تكتبها فيها. يُشكّل هذا الثنائي مبدأ PECS — أحد أكثر القواعد عملية في Java Generics.

المشكلة: الكتابة داخل مجموعة عامة

افترض أنك تريد كتابة دالة تملأ قائمة بأعداد صحيحة من عدّاد. قد يكون أول ما يخطر ببالك:

public static void fillWithIntegers(List<Number> list, int count) { for (int i = 0; i < count; i++) { list.add(i); } }

يُترجَم هذا الكود، لكنّه مقيّد جدًا. لا يمكنك تمرير List<Object> له رغم أن قائمة Object يمكنها بالتأكيد استيعاب الأعداد الصحيحة. الحل هو استخدام حدّ أدنى:

public static void fillWithIntegers(List<? super Integer> list, int count) { for (int i = 0; i < count; i++) { list.add(i); // آمن: Integer يتناسب مع أي نوع أعلى منه } }

الآن تقبل الدالة List<Integer>، وList<Number>، وList<Object> — أي قائمة نوع عناصرها هو Integer أو أي نوع سلف له في شجرة الوراثة.

لماذا الكتابة آمنة مع ? super

منطق المُترجم: إذا كانت القائمة تحتوي نوعًا ما هو نوع أعلى من Integer، فإن إضافة Integer إليها صحيحة دائمًا — لأن Integer هو نوع فرعي من Number الذي هو نوع فرعي من Object. يضمن النظام النوعي أن الإسناد آمن بغض النظر عن النوع الأعلى الفعلي للقائمة.

لماذا لا يمكن القراءة بأمان مع ? super: عند القراءة من List<? super Integer>، كل ما يعرفه المُترجم هو أن كل عنصر هو نوع أعلى مجهول من Integer — قد يكون Number أو Object أو أي شيء آخر. النوع الآمن الوحيد للقراءة هو Object، وهو ما يفقد كل المعلومات المفيدة. لهذا السبب يُقرن حرف البدل ذو الحدّ الأدنى بعمليات الكتابة (الاستهلاك)، لا القراءة (الإنتاج).

مثال عملي قابل للتشغيل

import java.util.ArrayList; import java.util.List; public class LowerBoundDemo { // يضيف أول n عدد صحيح غير سالب في أي قائمة // تقبل Integer أو أحد أنواعه الأعلى public static void fill(List<? super Integer> dest, int n) { for (int i = 0; i < n; i++) { dest.add(i); } } public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); List<Number> numList = new ArrayList<>(); List<Object> objList = new ArrayList<>(); fill(intList, 3); // صحيح: Integer super Integer fill(numList, 3); // صحيح: Number super Integer fill(objList, 3); // صحيح: Object super Integer System.out.println(intList); // [0, 1, 2] System.out.println(numList); // [0, 1, 2] System.out.println(objList); // [0, 1, 2] // هذا لن يُترجَم — Double ليس نوعًا أعلى من Integer: // List<Double> dblList = new ArrayList<>(); // fill(dblList, 3); // خطأ في الترجمة } }

PECS: المنتج يستخدم extends، والمستهلك يستخدم super

اختصار PECS (صاغه Joshua Bloch في كتاب Effective Java) يمنحك قاعدة بسيطة لاختيار حرف البدل المناسب:

  • إذا كان المعامل يُنتج قيمًا ستقرأها منه، استخدم ? extends T.
  • إذا كان المعامل يستهلك قيمًا ستكتبها فيه، استخدم ? super T.
  • إذا كنت تقرأ وتكتب معًا، استخدم النوع الصريح T بدون حرف بدل.

مثال كلاسيكي يجمع حرفَي البدل في دالة واحدة — نسخ العناصر من قائمة مصدر إلى قائمة وجهة:

public static <T> void copy(List<? extends T> src, List<? super T> dest) { for (T element : src) { // قراءة من المنتج src dest.add(element); // كتابة في المستهلك dest } }

اقرأ التوقيع بصوت عالٍ: "انسخ من قائمة تُنتج عناصر T (أو أنواعها الفرعية) إلى قائمة تستهلك عناصر T (أو أنواعها الأعلى)." هذا بالضبط ما يقوله PECS.

عند الشك، فكّر في اتجاه تدفق البيانات. البيانات تتدفق خارجًا من المنتج (extends) وداخلًا إلى المستهلك (super). إذا استطعت تحديد الاتجاه، يمكنك دائمًا اختيار حرف البدل الصحيح.

الاستخدام الفعلي في مكتبة JDK

تطبّق مكتبة JDK نفسها مبدأ PECS في java.util. انظر إلى Collections.sort:

public static <T extends Comparable<? super T>> void sort(List<T> list) { ... }

الحدّ Comparable<? super T> يعني أن المقارنة يكفي أن تكون معرّفة على سلف ما من أسلاف T — لا يلزم إعادة تعريفها في كل فئة فرعية. مثلًا، إذا كان Dog extends Animal وكان Animal implements Comparable<Animal>، يمكنك ترتيب List<Dog> لأن Animal هو نوع أعلى من Dog ويوفر compareTo بالفعل.

الأخطاء الشائعة

  • القراءة مع ? super وتوقّع نوع محدد: النوع المضمون الوحيد عند القراءة هو Object. إذا احتجت النوع المحدد فستضطر إلى تحويل نوع (cast)، وهو ما يُبطل الأمان النوعي.
  • استخدام ? super عندما يظهر معامل النوع في الاتجاهين: إذا كنت تقرأ وتكتب من خلال نفس المعامل، استخدم متغير نوع صريحًا بدلًا من حرف بدل.
  • الخلط بين الحدّ الأدنى والنوع الأصغر: ? super Integer يعني Integer أو ما هو أعلى في التسلسل الهرمي (Number، Object) — لا نوعًا أكثر تخصصًا. كلمة "super" تشير إلى الأنواع الأعلى.
لا يمكنك كتابة نوع فرعي عشوائي في حرف بدل ذي حدّ أدنى ما لم يكن مطابقًا للحدّ المعلَن. مثلًا، في List<? super Number> يمكنك إضافة Double (لأن Double يمتد من Number) لكن لا يمكنك إضافة Object عشوائي — سيرفضه المُترجم لأن Object لا يُضمن أنه Number.

الخلاصة

أحرف البدل ذات الحدود الدنيا (? super T) تجعل الدالة تقبل أي قائمة نوع عناصرها هو T أو سلف له، مما يتيح الكتابة بأمان. تذكّر PECS: استخدم ? extends حين تكون المجموعة منتجًا (تقرأ منها) و? super حين تكون مستهلكًا (تكتب فيها). على هذا المبدأ بالذات بُنيت أدوات المجموعات في مكتبة JDK.