Skip to content

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:

R call(T t)

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:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

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:

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

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:

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

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.