Skip to content

3. توقيعات الفئات والأساليب العامة

  

تحتوي مكتبة RxJava على العديد من الطرق التي تقبل مثيلات واجهات عامة كمعلمات. أحيانًا، تكون توقيعات هذه الطرق معقدة. وفيما يلي بعض الأمثلة:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)

public final <R> Observable<R> flatMap(Func1<? super T,? extends Observable<? extends R>> func)

تستخدم هاتان الطريقتان نوعين عامين هما T و R. ولكن ماذا يعني، على سبيل المثال، تعريف معلمة الطريقة map: Func1<? super T,? extends R> func ?

يمكن العثور على معلومات حول الأنواع العامة في URL [https://docs.oracle.com/javase/tutorial/java/generics/]. بعض المعلومات الواردة أدناه مستمدة من URL. ظهرت الأنواع العامة مع الإصدار 1.5 من Java.

لنتخيل خدمة ويب تقدم معلومات من أنواع مختلفة. قد تفشل خدمة الويب أحيانًا في تقديم هذه المعلومات، وعندها يتعين عليها الإبلاغ عن خطأ إلى العميل. يمكن عندئذٍ توحيد استجابة خدمة الويب بالشكل التالي:


public class Response<T> {

    // ----------------- الخصائص
    // حالة العملية
    private int status;
    // رسائل الخطأ المحتملة
    private List<String> messages;
    // نص الرد
    private T body;
...
}
  • السطر 1: يتم تعيين الفئة [Response] بالنوع T؛
  • السطر 9: النوع T هو نوع نص الرد، وهو ما يتوقعه العميل فعليًّا؛
  • السطر 5: رمز خطأ، 0 في حالة عدم وجود خطأ؛
  • السطر 7: رسائل تشرح الخطأ، null في حالة عدم وجود خطأ؛

من جانب العميل، يمكننا عندئذٍ كتابة:


Response<Product> product=getDataFromWebService(...) ;

أو


Response<List<Product>>=getDataFromWebService(...) ;

يتم استبدال النوع الشكلي T بنوع فعلي، وهو هنا [Product] أو [List<Product>]. وتُظهر الفئة العامة [Response<T>] فائدتها هنا لأنها تتيح العمل باستخدام استجابة قياسية.

لإنشاء نوع [Response] باستخدام عامل new، نكتب:


Response<List<Product>> response=new Response<List<Product>> (...) ;

ابتداءً من الإصدار 1.8 من Java، يمكن كتابة العبارة السابقة بطريقة أبسط:


Response<List<Product>> response=new Response<> (...) ;

لم يعد هناك داعٍ الآن لتحديد النوع الفعلي للمعلمات العامة على يمين علامة =؛ بل سيتم استخدام النوع الشكلي المحدد على يسار علامة =. ويُطلق على ذلك اسم «استدلال النوع»: حيث يستطيع المُجمِّع تحديد النوع الفعلي للمعلمات العامة بنفسه بناءً على السياق. وتُستخدم هذه الميزة بكثرة في دوال لامدا حيث غالبًا ما يتم حذف النوع الفعلي للمعلمات العامة لأي دالة.

أما على جانب العميل، فقد يكون للطريقة [getDataFromWebService] التوقيع التالي:


<T1,T2> Response<T1>  getDataFromWebService(String urlWebService, String httpMethod, T2 post)

لدينا هنا طريقة عامة معلمة بنوعين هما T1 و T2:

  • T1 هو نوع نص الاستجابة المتوقعة؛
  • T2 هو نوع القيمة المرسلة لعملية POST، وnull لعملية GET؛
  • urlWebService: هو URL الخاص بخدمة الويب؛
  • httpMethod: هي الطريقة HTTP أو GET أو POST التي يجب استخدامها للاستعلام عن هذه العملية URL؛

من جانب العميل، يمكن إجراء الاستدعاء التالي:


Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;

في هذه الحالة، سيكون لدينا T1=Product و T2=Long.

مع Java 8، يمكننا استخدام استدلال الأنواع، وسنكتب بشكل أبسط:


Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;

فيما يلي استدعاء آخر ممكن:


Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;

في هذه الحالة، سيكون لدينا T1=List<Product> و T2=Long[].

يمكن للفئة أو الطريقة العامة فرض قيود على معلماتها العامة. لنعد إلى تعريف الطريقة [map] من RxJava:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)

تقبل الطريقة نوعين من المعلمات، T و R. النوع [Func1] هو واجهة عامة:

 

يُعرّف النوع Func1 واجهة وظيفية، أي واجهة تحتوي على دالة واحدة فقط. تشكل الواجهات الوظيفية أساس دوال لامدا في Java 8. هنا، تُعرّف دالة الواجهة على النحو التالي:

R call(T t)

إذن، T هو نوع المعلمة التي يتم تمريرها إلى [callوR هو نوع النتيجة التي يتم الحصول عليها. لنعد إلى تعريف الطريقة [map]:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)
  • تتوقع الطريقة [map] معلمة واحدة من النوع [Func1<? super T,? extends R>] وتُرجع نوعًا من النوع [Observable<R>]؛
  • ? super T: يجب أن يكون المعامل الذي يتم تمريره إلى الدالة [Func1.call] من النوع T أو من نوع أعلى من T؛
  • ? extends R: يجب أن يكون نوع النتيجة التي تُرجعها الطريقة [call] من النوع R أو مشتقًا منه إذا كانت R فئة، أو أن تُنفِّذ R إذا كانت R واجهة؛

لفهم القيدين السابقين بشكل أفضل، لنأخذ أمثلة مأخوذة من المرجع [https://docs.oracle.com/javase/tutorial/java/generics/bounded.html]:


package tests;

public class Box<T> {

    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u) {
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.set(new Integer(10));
        integerBox.inspect(new Long(20));
        integerBox.inspect(new Double(-1.78));
        //integerBox.inspect("some text"); // خطأ: هذا لا يزال من نوع String!
    }
}
  • السطر 3: يتم تعريف الفئة [Box] بالنوع T الذي يمثل نوع الحقل في السطر 5؛
  • السطر 15: تقبل الطريقة [inspect] معلمة من النوع U. يمكن أن تحتوي الطريقة أيضًا على معلمات عامة. وفي هذه الحالة، يتم إعلانها على النحو التالي:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

هنا، يُعلن U كنوع عام للطريقة بواسطة <U> ولكن مع قيد <U extends Number>، أي أن U يجب أن يمتد من الفئة [Number]. وبسبب هذا القيد، يُشير المُجمِّع إلى وجود خطأ في السطر 25؛

  • السطران 23-24: استدعاءات للطريقة [inspect] بأنواع مشتقة من [Number

ونحصل على النتائج التالية:

T: java.lang.Integer
U: java.lang.Long
T: java.lang.Integer
U: java.lang.Double

ملاحظة: لتنفيذ الفئة في IntelliJ، اتبع الخطوات التالية [1, 2]:

 

الآن، لننظر إلى المثال التالي:


    public void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 3; i++) {
            list.add(i);
        }
}

تقبل الطريقة [addNumbers] كمعلمة نوع List<T> حيث T هي فئة أم للفئة [Integer]. بسبب هذا القيد، يمكن إضافة الأعداد الصحيحة من 1 إلى 3 إلى القائمة (الأسطر 2-4) ويمكن استدعاء الدالة بالطريقة التالية:


    List<Number> numbers=new ArrayList<>();
    // إضافة عدد من النوع Double <-- Double
    numbers.add(7.8);
    // إضافة عدد من نوع Long <-- Long
    numbers.add(1L);
    // إضافة List<Number> Number <-- Integer
    addNumbers(numbers);
    // عرض List<Number>
    for(Number number : numbers){
      System.out.println(number.getClass().getName());
}

ونحصل عندئذٍ على النتيجة التالية:

1
2
3
4
5
java.lang.Double
java.lang.Long
java.lang.Integer
java.lang.Integer
java.lang.Integer

لتوسيع فئة Box<T>، نكتب:


class OtherBox<T> extends Box<T> {

}

ويمكن للفئة الفرعية أن تُدخل هي نفسها معلمات عامة جديدة:


class AnotherBox<T, T1> extends Box<T> {
    void consumer(T1 t1) {
        System.out.printf("%s%n", t1);
    }
}

ويمكن للواجهة أيضًا استخدام المعلمات العامة:


interface I<T> {
    void doSomethingWith(T t);
}

لتنفيذ ذلك، نستخدم الصيغة التالية:


class A<T> implements I<T> {

    @Override
    public void doSomethingWith(T t) {
        System.out.printf("%s%n", t);
    }

}

لقد أصبح لدينا الآن معرفة كافية بالأنماط العامة لنتطرق إلى دوال لامدا.