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. Тут метод інтерфейсу визначено так:
Отже, 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. Метод також може мати генеричні параметри. У цьому випадку він оголошується наступним чином:
Тут U оголошено як генеричний тип методу за допомогою <U>, але з обмеженням <U extends Number>, тобто U має бути похідним від класу [Number]. Через це обмеження компілятор повідомляє про помилку у рядку 25;
- рядки 23–24: виклики методу [inspect] із типами, похідними від [Number];
Отримуємо такі результати:
Примітка: щоб запустити клас в 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());
}
У результаті отримуємо такий результат:
Щоб розширити клас 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);
}
}
Тепер ми знаємо про генералізовані типи достатньо, щоб перейти до лямбда-функцій.


