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

الفئات العامة (Generic Classes)

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

الفئات العامة (Generic Classes)

الفئة العامة هي فئة مُعرَّفة بمُعامل نوع (type parameter). بدلًا من تحديد نوع ثابت مثل String أو Integer، تُعلن عن عنصر نائب يملأه المُستدعي عند الاستخدام. يتحقّق المُترجم بعد ذلك من صحة كل استخدام، مُزيلًا أخطاء التحويل التي كانت شائعة قبل Java 5.

لماذا نكتب فئة عامة؟

افترض أنّك تحتاج إلى حاوية بسيطة تحمل قيمة واحدة وتُعيدها. بدون الجنيريكس أمامك خياران سيئان: كتابة فئة مستقلة لكل نوع (StringBox، IntBox، …) أو استخدام Object مع التحويل اليدوي في كل مرة. كلاهما مُرهِق. الفئة العامة تحلّ المشكلتين دفعةً واحدة.

الفكرة الأساسية: تُعرِّف الفئة العامة السلوك مرّة واحدة؛ ويُنتج المُترجم نسخة آمنة من حيث النوع لكل استخدام ملموس. تحصل على إعادة الاستخدام والأمان في آنٍ معًا.

الإعلان عن فئة عامة

ضَع قائمة مُعامِلات النوع — اسم واحد أو أكثر بين أقواس الزاوية — مباشرةً بعد اسم الفئة:

public class Box<T> { private T value; public Box(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } }

T مجرّد اسم؛ بالاتفاق تُستخدم أحرف كبيرة مفردة: T للنوع، E للعنصر، K وV للمفتاح والقيمة، R لنوع الإرجاع. يستبدل المُترجم T بما يُوفّره المُستدعي.

استخدام الفئة العامة

عند موقع الاستدعاء تُوفّر وسيطة النوع بين أقواس الزاوية عند إنشاء الكائن:

Box<String> nameBox = new Box<>("Alice"); // عامل الماسة يستنتج String Box<Integer> ageBox = new Box<>(30); String name = nameBox.getValue(); // لا حاجة للتحويل int age = ageBox.getValue(); // فكّ التغليف التلقائي // المُترجم يرفض النوع الخاطئ فورًا: // nameBox.setValue(99); // خطأ تجميعي: int ليس String
عامل الماسة <>: منذ Java 7 يمكنك حذف وسيطة النوع على الجانب الأيمن من الإعلان — يستنتجها المُترجم من الجانب الأيسر. استخدم دائمًا new Box<>(...) عوضًا عن new Box<String>(...) لتقليل التكرار.

مُعامِلات نوع متعددة

يمكن للفئة أن تحمل أكثر من مُعامِل نوع. مثال كلاسيكي هو الزوجية التي تحمل قيمتين مستقلتين:

public class Pair<A, B> { private final A first; private final B second; public Pair(A first, B second) { this.first = first; this.second = second; } public A getFirst() { return first; } public B getSecond() { return second; } @Override public String toString() { return "(" + first + ", " + second + ")"; } }
Pair<String, Integer> entry = new Pair<>("score", 95); System.out.println(entry.getFirst()); // score System.out.println(entry.getSecond()); // 95 Pair<String, String> coords = new Pair<>("lat", "lng");

الفئات العامة وأعضاؤها

يمكن لكل تابع نموذجي (instance method) داخل الفئة العامة استخدام مُعامِلات النوع الخاصة بمستوى الفئة. يعمل مُعامِل النوع كأنّه نوع ملموس لكامل النموذج:

public class Stack<E> { private final java.util.ArrayDeque<E> deque = new java.util.ArrayDeque<>(); public void push(E item) { deque.push(item); } public E pop() { return deque.pop(); } public E peek() { return deque.peek(); } public boolean isEmpty() { return deque.isEmpty(); } }
Stack<String> words = new Stack<>(); words.push("hello"); words.push("world"); System.out.println(words.pop()); // world System.out.println(words.pop()); // hello

الأعضاء الساكنة لا تستطيع استخدام مُعامِل نوع الفئة

خطأ شائع: الحقول الساكنة (static fields) والتوابع الساكنة (static methods) تنتمي إلى الفئة لا إلى نموذج منها، لذا لا يوجد مُعامِل نوع مرتبط بها. سيرفض المُترجم private static T cache; داخل فئة عامة. إن احتجت إلى أداة مساعدة ساكنة عامة، أعلن عن تابع عام بدلًا من ذلك (موضوع الدرس التالي).

الأنواع الخام — ما يجب تجنّبه

إن استخدمت فئة عامة دون وسيطة نوع، تسمح Java بذلك لأسباب التوافق مع الإصدارات القديمة، لكنك تفقد كل أمان النوع:

Box rawBox = new Box("text"); // يُجمَّع مع تحذير String s = (String) rawBox.getValue(); // تحويل يدوي مطلوب — عرضة للأخطاء

لا تستخدم الأنواع الخام في الكود الجديد. وُجدت فقط للتعامل مع مكتبات ما قبل Java 5. ستُنبّه بيئات التطوير الحديثة عليها بتحذيرات وستعدّها أدوات التحليل الساكن أخطاءً.

تطبيق متكامل — نوع Result العام

إليك نمطًا واقعيًا: Result<T> يُغلّف إمّا قيمة نجاح أو رسالة خطأ، دون إطلاق استثناءات:

public class Result<T> { private final T value; private final String error; private Result(T value, String error) { this.value = value; this.error = error; } public static <T> Result<T> success(T value) { return new Result<>(value, null); } public static <T> Result<T> failure(String error) { return new Result<>(null, error); } public boolean isSuccess() { return error == null; } public T getValue() { return value; } public String getError() { return error; } }
Result<Integer> ok = Result.success(42); Result<Integer> err = Result.failure("not found"); if (ok.isSuccess()) { System.out.println("Value: " + ok.getValue()); }
لاحظ: تستخدم توابع المصنع الساكنة إعلانها الخاص <T> (تابع عام) لأن السياق الساكن لا يستطيع الوصول إلى T الخاصة بالفئة. النقطة المهمة هنا هي كيف يتدفّق مُعامِل النوع على مستوى الفئة بشكل طبيعي عبر جميع التوابع النموذجية.

خلاصة

  • أعلن عن مُعامِلات النوع بعد اسم الفئة: class Foo<T>.
  • استخدم عامل الماسة <> على الجانب الأيمن ليستنتج المُترجم النوع.
  • يُسمح بمُعامِلات متعددة: class Pair<A, B>.
  • يستطيع أعضاء النموذج استخدام مُعامِل النوع بحرية؛ الأعضاء الساكنة لا تستطيع ذلك.
  • تجنّب الأنواع الخام — قدّم دائمًا وسيطة نوع.