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. Генератори з’явилися у версії Java 1.5.

Уявімо собі веб-сервіс, який надає інформацію різних типів. Іноді веб-сервіс може не змогти надати цю інформацію і тоді повинен повідомити клієнту про помилку. У такому разі можна стандартизувати відповідь веб-сервісу у такому вигляді:


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>> (...) ;

Починаючи з версії Java 1.8, попередню інструкцію можна записати простіше:


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"); // помилка: це все ще рядок!
    }
}
  • рядок 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]. Через це обмеження до списку можна додати цілі числа int від 1 до 3 (рядки 2–4), і метод можна викликати таким чином:


    List<Number> numbers=new ArrayList<>();
    // додано число типу Double <-- Double
    numbers.add(7.8);
    // додавання числа типу Long <-- Long
    numbers.add(1L);
    // додавання List<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);
    }

}

Тепер ми знаємо про генералізовані типи достатньо, щоб перейти до лямбда-функцій.