Skip to content

3. امضاهای کلاس‌ها و متدهای عمومی

  

کتابخانه RxJava دارای متدهای متعددی است که نمونه‌هایی از رابط‌های عمومی (generic) را به عنوان پارامتر می‌پذیرند. گاهی اوقات، امضاهای این متدها پیچیده هستند. در اینجا چند مثال آورده شده است:


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&lt;? super T, ? extends R&gt; func ?

اطلاعاتی در مورد جنریک‌ها را می‌توان در URL [https://docs.oracle.com/javase/tutorial/java/generics/] یافت. بخشی از اطلاعات زیر از URL گرفته شده است. جنریک‌ها در نسخه ۱.۵ جاوا معرفی شدند.

بیایید یک سرویس وب را تصور کنیم که اطلاعاتی از انواع مختلف را بازمی‌گرداند. این سرویس وب ممکن است گاهی در بازگرداندن این اطلاعات ناکام بماند و بنابراین باید خطایی را به کلاینت خود گزارش دهد. سپس می‌توانیم پاسخ سرویس وب را به شکل زیر استاندارد کنیم:


public class Response<T> {

    // ----------------- ویژگی‌ها
    // وضعیت عملیات
    private int status;
    // هر پیام خطا
    private List<String> messages;
    // بدنه پاسخ
    private T body;
...
}
  • خط ۱: کلاس [Response] با نوع T پارامترگذاری شده است؛
  • خط ۹: نوع T نوع بدنه پاسخ است، که همان چیزی است که کلاینت واقعاً انتظار دارد؛
  • خط ۵: یک کد خطا، ۰ در صورت عدم وجود خطا؛
  • خط ۷: پیام‌هایی که خطا را توضیح می‌دهند؛ 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>> (...) ;

از نسخهٔ ۱.۸ جاوّا، عبارت بالا را می‌توان ساده‌تر این‌گونه نوشت:


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

دیگر نیازی نیست که نوع واقعی پارامترهای جنریک را در سمت راست علامت مساوی مشخص کنید: در عوض، از نوع صوری مشخص‌شده در سمت چپ علامت مساوی استفاده خواهد شد. این پدیده به «استنتاج نوع» (type inference) معروف است: کامپایلر خود، بر اساس زمینه، می‌تواند نوع واقعی پارامترهای جنریک را تعیین کند. این ویژگی به طور گسترده در توابع لامبدا (lambda functions) استفاده می‌شود، جایی که نوع واقعی پارامترهای جنریک یک متد اغلب حذف می‌شود.

اکنون، در سمت کلاینت، متد [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.

با جاوا ۸ می‌توان از استنتاج نوع استفاده کرد، بنابراین می‌توانیم ساده‌تر بنویسیم:


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 یک رابط تابعی را تعریف می‌کند، یعنی رابطی با تنها یک متد. رابط‌های تابعی اساس عبارت‌های لامبدا در جاوا ۸ را تشکیل می‌دهند. در اینجا، متد این رابط به صورت زیر تعریف شده است:

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 را پیاده‌سازی کند اگر 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"); // خطا: این هنوز یک رشته است!
    }
}
  • خط ۳: کلاس [Box] با نوع T پارامتریزه شده است، که این نوع، نوع فیلد در خط ۵ است؛
  • خط ۱۵: متد [inspect] یک پارامتر از نوع U را می‌پذیرد. یک متد همچنین می‌تواند پارامترهای عمومی داشته باشد. در این صورت به شکل زیر اعلام می‌شود:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

در اینجا، U به‌عنوان نوع عمومی متد با استفاده از <U> اما با قید <U extends Number> اعلام شده است، یعنی U باید از کلاس [Number] ارث ببرد. به دلیل این قید، کامپایلر در خط ۲۵ خطا گزارش می‌کند؛

  • خطوط ۲۳–۲۴: فراخوانی متد [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] است. به دلیل این محدودیت، اعداد صحیح int 1 تا 3 را می‌توان به لیست اضافه کرد (خطوط 2–4) و متد را می‌توان به صورت زیر فراخوانی کرد:


    List<Number> numbers=new ArrayList<>();
    // افزودن یک عدد دوبل <-- Double
    numbers.add(7.8);
    // اضافه کردن یک Long به یک Number <-- 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&lt;T&gt;، می‌نویسیم:


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);
    }

}

اکنون به اندازه کافی درباره جنریک‌ها می‌دانیم تا به توابع لامبدا بپردازیم.