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

التوابع الجنيسة (Generic Methods)

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

التوابع الجنيسة (Generic Methods)

في الدرس السابق رأيت كيف تجعل صنفًا كاملًا جنيسًا. لكن أحيانًا تحتاج فقط إلى تابع واحد يكون جنيسًا — بينما يبقى باقي الصنف محدّد النوع. تتيح لك Java الإعلان عن معاملات نوع مباشرةً على التابع، بمعزل عن كون الصنف المحيط جنيسًا أم لا. يمنحك ذلك أدوات مساعدة خفيفة وقابلة لإعادة الاستخدام دون الالتزام بصنف جنيس كامل.

الصياغة: أين تُوضع معاملات النوع

تأتي قائمة معاملات النوع بين المُعدِّلات ونوع القيمة المُعادة:

public <T> T identity(T value) { return value; }

تحليل الصياغة:

  • <T> — يُعلن عن معامل النوع لهذا التابع فحسب.
  • T الأولى بعد <T> هي نوع القيمة المُعادة.
  • المعامل T value يستخدم النوع ذاته.

يمكنك أن تملك معاملات نوع متعددة:

public <K, V> Map.Entry<K, V> pair(K key, V value) { return Map.entry(key, value); }

استنتاج النوع: المُصرِّف يؤدي العمل

لا تكاد تحتاج إلى تحديد وسيط النوع صراحةً عند استدعاء تابع جنيس — فالمُصرِّف يستنتجه من الوسيط الذي تمرّره:

public class Utils { public static <T> T identity(T value) { return value; } public static void main(String[] args) { String s = identity("hello"); // T يُستنتج على أنه String Integer n = identity(42); // T يُستنتج على أنه Integer // استدعاء صريح — نادرًا ما تحتاجه، لكنه ممكن: String s2 = Utils.<String>identity("world"); } }
كيف يعمل الاستنتاج؟ يفحص المُصرِّف نوع كل وسيط ويوحّده مع أنواع المعاملات المُعلنة. في identity("hello") الوسيط من نوع String فيكون T = String. في identity(42) النوع المعبَّأ هو Integer فيكون T = Integer. إن تعارضت الوسيطات وسَّع المُصرِّف النوع إلى أب مشترك (أو Object كملاذ أخير).

مثال عملي: تابع تبادل جنيس

افترض أنك تريد تبادل عنصرين في قائمة. بدون الجنيسية ستكتب تابعًا لكل نوع. بالتابع الجنيس تكتبه مرة واحدة:

import java.util.List; public class Collections2 { public static <T> void swap(List<T> list, int i, int j) { T temp = list.get(i); list.set(i, list.get(j)); list.set(j, temp); } public static void main(String[] args) { var names = new java.util.ArrayList<>(List.of("Alice", "Bob", "Carol")); var numbers = new java.util.ArrayList<>(List.of(10, 20, 30)); swap(names, 0, 2); // T = String swap(numbers, 0, 2); // T = Integer System.out.println(names); // [Carol, Bob, Alice] System.out.println(numbers); // [30, 20, 10] } }
التوابع الجنيسة الساكنة (static) شائعة جدًا لأنها لا تستطيع الاعتماد على معامل نوع النسخة. صنف java.util.Collections مليء بها — فـsort وbinarySearch وunmodifiableList كلها توابع جنيسة ساكنة.

إعادة نوع جنيس

يستطيع التابع الجنيس إنتاج قيمة جديدة لا مجرد العمل على وسيطاته. نمط شائع هو المصنع أو مساعد التحويل:

import java.util.ArrayList; import java.util.List; public class ListFactory { // تُعيد ArrayList فارغة وقابلة للتعديل من نوع List<T> public static <T> List<T> emptyMutable() { return new ArrayList<>(); } public static void main(String[] args) { List<String> strings = emptyMutable(); // T = String List<Double> doubles = emptyMutable(); // T = Double } }

يستنتج المُصرِّف T من نوع الهدف في جانب الإسناد. يُسمّى ذلك استنتاج النوع الهدفي وقد تعزّز في Java 8.

التوابع الجنيسة داخل الأصناف الجنيسة

يمكن للتابع الجنيس أن يتواجد داخل صنف جنيس، ومعامل نوعه مستقل تمامًا عن معامل نوع الصنف:

public class Box<T> { // معامل نوع الصنف T private T value; public Box(T value) { this.value = value; } public T get() { return value; } // معامل نوع التابع U — مختلف عن T public <U> Box<U> map(java.util.function.Function<T, U> mapper) { return new Box<>(mapper.apply(value)); } public static void main(String[] args) { Box<String> strBox = new Box<>("42"); Box<Integer> intBox = strBox.map(Integer::parseInt); System.out.println(intBox.get()); // 42 } }
T مقابل U: داخل Box<T> يُثبَّت T عند إنشاء نسخة من Box. أما U فيُستنتج من جديد في كل استدعاء لـmap. يعيش كل من المعاملين في نطاق مختلف ولا يتعارضان.

متى تستخدم تابعًا جنيسًا مقابل صنف جنيس

  • استخدم تابعًا جنيسًا حين تكون علاقة النوع محلية لعملية واحدة — ولا داعي لتذكّرها بين الاستدعاءات.
  • استخدم صنفًا جنيسًا حين يكون النوع جزءًا من هوية الكائن ويجب أن يبقى ثابتًا عبر استدعاءات متعددة (كـStack<T> الذي يحتفظ بنوع ما بداخله).
خطأ شائع — إخفاء معامل نوع الصنف: إن كان الصنف الجنيس يمتلك بالفعل <T> وأعلنت بالخطأ public <T> void foo(T x) على تابع ما، فإن T الخاصة بالتابع تُخفي T الخاصة بالصنف بصمت. ستُحذّرك معظم بيئات التطوير المتكاملة. استخدم حرفًا مختلفًا (مثل U) لمعاملات النوع على مستوى التابع داخل الأصناف الجنيسة.

الخلاصة

تتيح لك التوابع الجنيسة كتابة خوارزمية واحدة آمنة على مستوى الأنواع تعمل مع أنواع متعددة، دون جعل الصنف كله جنيسًا. تُعلن معامل النوع قبيل نوع القيمة المُعادة مباشرةً، ويستنتجه المُصرِّف من الوسيطات أو هدف الإسناد. هذا النمط موجود في كل مكان في المكتبة القياسية وفي أصناف الأدوات المُصمَّمة بعناية.