3. Sygnatury klas i metod generycznych
![]() |
Biblioteka RxJava zawiera wiele metod przyjmujących jako parametry instancje interfejsów generycznych. Czasami ich sygnatura jest skomplikowana. Oto kilka przykładów:
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)
Te dwie metody wykorzystują dwa typy generyczne T i R. Ale co oznacza na przykład definicja parametru metody map: Func1<? super T,? extends R> func ?
Informacje na temat typów generycznych można znaleźć w URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Niektóre informacje poniżej pochodzą z tego źródła: URL. Typy generyczne pojawiły się wraz z wersją 1.5 języka Java.
Wyobraźmy sobie serwis internetowy dostarczający informacje różnego rodzaju. Czasami serwis ten może nie zdołać dostarczyć tych informacji i musi wówczas zgłosić błąd swojemu klientowi. Odpowiedź serwisu internetowego można wówczas ujednolicić w następującej postaci:
public class Response<T> {
// ----------------- właściwości
// status operacji
private int status;
// ewentualne komunikaty o błędach
private List<String> messages;
// treść odpowiedzi
private T body;
...
}
- wiersz 1: klasa [Response] jest definiowana przez typ T;
- wiersz 9: typ T jest typem treści odpowiedzi, czyli tym, czego faktycznie oczekuje klient;
- wiersz 5: kod błędu, 0, jeśli nie ma błędu;
- wiersz 7: komunikaty wyjaśniające błąd, null, jeśli nie ma błędu;
Po stronie klienta można wówczas zapisać:
Response<Product> product=getDataFromWebService(...) ;
lub
Response<List<Product>>=getDataFromWebService(...) ;
Typ formalny T zostaje zastąpiony typem efektywnym, w tym przypadku [Product] lub [List<Product>]. Klasa generyczna [Response<T>] okazuje się tutaj przydatna, ponieważ umożliwia pracę ze standardową odpowiedzią.
Aby utworzyć typ [Response] za pomocą operatora new, należy wpisać:
Response<List<Product>> response=new Response<List<Product>> (...) ;
Od wersji 1.8 języka Java powyższą instrukcję można zapisać w prostszy sposób:
Response<List<Product>> response=new Response<> (...) ;
Nie ma już potrzeby określania po prawej stronie znaku = rzeczywistego typu parametrów generycznych: zostanie przyjęty typ formalny określony po lewej stronie znaku =. Nazywa się to wnioskowaniem typu: kompilator jest w stanie samodzielnie ustalić rzeczywisty typ parametrów generycznych na podstawie kontekstu. Ta cecha jest szeroko wykorzystywana w funkcjach lambda, gdzie rzeczywisty typ parametrów generycznych metody jest często pomijany.
Po stronie klienta metoda [getDataFromWebService] mogłaby mieć następującą sygnaturę:
<T1,T2> Response<T1> getDataFromWebService(String urlWebService, String httpMethod, T2 post)
Mamy tu metodę generyczną, której parametrami są dwa typy: T1 i T2:
- T1 to typ treści oczekiwanej odpowiedzi;
- T2 to typ wartości wysyłanej dla operacji POST, a null dla operacji GET;
- urlWebService: jest to URL usługi internetowej;
- httpMethod: jest metodą HTTP, GET lub POST, którą należy zastosować w celu wysłania zapytania do tej operacji URL;
Po stronie klienta może pojawić się następujące wywołanie:
Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
W tym przypadku otrzymamy T1=Product oraz T2=Long.
W Javie 8 można skorzystać z wnioskowania o typach i zapisać to w prostszy sposób:
Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
Oto inne możliwe wywołanie:
Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;
W tym przypadku otrzymamy T1=List<Product> oraz T2=Long[].
Klasa lub metoda generyczna może nakładać ograniczenia na swoje parametry generyczne. Wróćmy do definicji metody [map] z RxJava:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
Metoda przyjmuje dwa typy parametrów, T i R. Typ [Func1] jest interfejsem generycznym:
![]() |
Func1 definiuje interfejs funkcjonalny, tj. interfejs posiadający tylko jedną metodę. Interfejsy funkcjonalne stanowią podstawę funkcji lambda w Javie 8. W tym przypadku metoda interfejsu jest zdefiniowana jako:
Zatem T jest typem parametru przekazanego do [call], a R typem uzyskanego wyniku. Wróćmy do definicji metody [map]:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
- metoda [map] oczekuje pojedynczego parametru typu [Func1<? super T,? extends R>] i zwraca typ [Observable<R>];
- ? super T: parametr przekazywany do metody [Func1.call] musi być typu T lub nadrzędnego względem T;
- ? extends R: typ wyniku zwracanego przez metodę [call] musi być typu R lub pochodny, jeśli R jest klasą, albo musi implementować R, jeśli R jest interfejsem;
Aby lepiej zrozumieć oba powyższe ograniczenia, rozważmy przykłady zaczerpnięte z dokumentacji metody [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"); // błąd: to nadal jest String!
}
}
- wiersz 3: klasa [Box] jest parametryzowana przez typ T, który jest typem pola w wierszu 5;
- wiersz 15: metoda [inspect] przyjmuje parametr typu U. Metoda może również posiadać parametry generyczne. Wówczas jest ona deklarowana w następujący sposób:
W tym przypadku U jest zadeklarowane jako typ generyczny metody za pomocą <U>, ale z ograniczeniem <U extends Number>, co oznacza, że U musi rozszerzać klasę [Number]. Z powodu tego ograniczenia kompilator zgłasza błąd w wierszu 25;
- wiersze 23–24: wywołania metody [inspect] z typami pochodnymi od [Number];
Otrzymujemy następujące wyniki:
Uwaga: aby uruchomić klasę w IntelliJ, należy postępować w następujący sposób dla [1, 2]:
![]() |
Rozważmy teraz następujący przykład:
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
Metoda [addNumbers] przyjmuje jako parametr typ List<T>, gdzie T jest klasą nadrzędną klasy [Integer]. Z powodu tego ograniczenia do listy można dodać liczby całkowite od 1 do 3 (wiersze 2–4), a metoda mogłaby zostać wywołana w następujący sposób:
List<Number> numbers=new ArrayList<>();
// dodano liczbę typu Double <-- Double
numbers.add(7.8);
// dodanie liczby typu Long <-- Long
numbers.add(1L);
// dodanie List<Number> Number <-- Integer
addNumbers(numbers);
// wyświetlenie List<Number>
for(Number number : numbers){
System.out.println(number.getClass().getName());
}
W rezultacie otrzymujemy następujący wynik:
Aby rozszerzyć klasę Box<T>, należy napisać:
class OtherBox<T> extends Box<T> {
}
Klasa potomna może sama wprowadzać nowe parametry generyczne:
class AnotherBox<T, T1> extends Box<T> {
void consumer(T1 t1) {
System.out.printf("%s%n", t1);
}
}
Również interfejs może wykorzystywać parametry generyczne:
interface I<T> {
void doSomethingWith(T t);
}
Aby go zaimplementować, stosuje się następującą składnię:
class A<T> implements I<T> {
@Override
public void doSomethingWith(T t) {
System.out.printf("%s%n", t);
}
}
Wiemy już wystarczająco dużo o typach generycznych, aby przejść do funkcji lambda.


