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

معاملات النوع المقيّدة

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

معاملات النوع المقيّدة

حتى الآن كانت معاملات النوع التي رأيناها — مثل <T> — تقبل أي نوع. هذه المرونة مفيدة، لكنّك في أحيان كثيرة تحتاج إلى تقييد الأنواع المسموح بها، حتى تتمكّن من استدعاء دوال محدّدة على القيمة داخل الكود العام. وهذا تمامًا ما تفعله معاملات النوع المقيّدة.

المشكلة بدون قيود

تخيّل أنّك تريد كتابة دالة تجد القيمة الأكبر من قيمتين. قد تبدو محاولتك الأولى كالتالي:

// لا تُترجَم — T لا تملك compareTo() public static <T> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; // خطأ: T لا تملك الدالة compareTo }

يرفض المُترجم هذا لأن T قد تكون أي شيء — Thread، أو فئة Car مخصّصة، أو أي شيء آخر. لا شيء منها مضمون دعمه لـ compareTo. عليك إخبار المُترجم بأن T يجب أن تُنفّذ Comparable.

القيد extends

تقيّد معامل النوع بكتابة <T extends SomeType>. يعني هذا أن T يجب أن تكون SomeType نفسها أو نوعًا فرعيًا منها — سواء كانت SomeType فئةً أم واجهةً. الكلمة الدائمة هي extends حتى للواجهات.

public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; } // تعمل مع Integer وDouble وString — كلها تُنفّذ Comparable System.out.println(max(3, 7)); // 7 System.out.println(max("apple", "fig")); // fig

داخل جسم الدالة يعرف المُترجم الآن أن كل T تملك دالة compareTo، فيكون الاستدعاء آمنًا.

extends للفئات والواجهات معًا: في القيد، تشمل extends كلًّا من الوراثة من فئة والتطبيق لواجهة. تكتب <T extends Runnable> حتى وإن كانت Runnable واجهة — لا توجد كلمة implements في قيود معاملات النوع.

فئة عامة مع قيد

تعمل القيود كذلك على معاملات النوع على مستوى الفئة. إليك NumberBox التي لا تقبل إلا الأنواع الرقمية وتُعيد القيمة كـ double:

public class NumberBox<T extends Number> { private final T value; public NumberBox(T value) { this.value = value; } public T getValue() { return value; } // آمن لأن T مضمون أنها ترث من Number public double asDouble() { return value.doubleValue(); } } // الاستخدام NumberBox<Integer> intBox = new NumberBox<>(42); NumberBox<Double> dblBox = new NumberBox<>(3.14); System.out.println(intBox.asDouble()); // 42.0 System.out.println(dblBox.asDouble()); // 3.14 // NumberBox<String> strBox = new NumberBox<>("hi"); // خطأ في الترجمة

خطأ الترجمة في السطر الأخير هو ما نريده تمامًا — يعمل القيد كحاجز في وقت الترجمة لا في وقت التشغيل.

قيود متعدّدة

يمكن لمعامل النوع أن يملك أكثر من قيد واحد. الصيغة تستخدم & للفصل بينها:

<T extends ClassBound & InterfaceOne & InterfaceTwo>

تنطبق هنا قاعدتان:

  1. يُسمح بقيد فئة واحدة على الأكثر، ويجب أن يأتي أولًا.
  2. جميع القيود المتبقية يجب أن تكون واجهات.

إليك مثالًا واقعيًا. لنفترض أنّك تكتب أداة مساعدة تحتاج كائنات تُنفّذ Serializable وComparable معًا — لفرز عناصر ثم حفظها مثلًا:

import java.io.Serializable; import java.util.List; import java.util.Collections; public static <T extends Comparable<T> & Serializable> T findMax(List<T> list) { if (list.isEmpty()) { throw new IllegalArgumentException("List must not be empty"); } return Collections.max(list); } // Integer تُنفّذ Comparable<Integer> وSerializable — تحقّق كلا القيدين List<Integer> numbers = List.of(3, 1, 9, 2, 7); System.out.println(findMax(numbers)); // 9
قيد الفئة يجب أن يأتي أولًا: كتابة <T extends Serializable & AbstractBase> حيث AbstractBase فئة لن تُترجَم إذا ظهرت بعد الواجهة. ضع الفئة دائمًا أولًا: <T extends AbstractBase & Serializable>.

القيود في التعريفات العامة التعاودية

ربّما لاحظت استخدام Comparable<T> قيدًا بدلًا من Comparable الخام. يُسمّى هذا قيد النوع التعاودي وهو مهم. يقول: لا يمكن مقارنة T إلا بـ T أخرى، لا بأي نوع عشوائي. يُبقي هذا المقارنات آمنة من حيث النوع:

// بدون قيد تعاودي: يمكن مقارنة T بأي شيء — فضفاض <T extends Comparable> T badMax(T a, T b) { ... } // مع قيد تعاودي: T لا تُقارَن إلا بـ T أخرى — صحيح <T extends Comparable<T>> T goodMax(T a, T b) { ... }

ستجد هذا النمط في كل مكان في المكتبة القياسية لـ Java — تستخدم Collections.sort النمط <T extends Comparable<? super T>> للسبب ذاته.

متى تستخدم القيود

  • استخدم القيد حين يحتاج كودك العام إلى استدعاء دالة لا تملكها إلا أنواع محدّدة (مثل compareTo أو doubleValue).
  • استخدم قيودًا متعدّدة حين تحتاج تركيب قدرات (يجب أن يكون قابلًا للترتيب وللتسلسل في آنٍ واحد).
  • احرص على أن تكون القيود ضيّقة بما يكفي فقط. القيد المُضيَّق أكثر من اللازم يُقيّد المستخدمين دون مبرّر.
فضّل قيود الواجهات على قيود الفئات. الارتباط بفئة ملموسة (مثل <T extends ArrayList>) يجعل كودك هشًّا وغالبًا يُشير إلى أنك بحاجة لتصميم مختلف. الارتباط بواجهة (مثل <T extends List>) أكثر مرونة بكثير.

الخلاصة

تتيح لك معاملات النوع المقيّدة تضييق مجموعة الأنواع التي يمكن استبدالها بمتغيّر النوع. تعمل كلمة extends لكلٍّ من قيود الفئة والواجهة. تُدمج القيود المتعدّدة باستخدام & مع وضع قيد الفئة (إن وُجد) أولًا. تُطلق القيود استدعاءات الدوال المستحيلة على T غير المقيّدة، محوّلةً الكود العام من مجرّد مرن إلى مفيد حقًّا.