Skip to content

3. Signaturen generischer Klassen und Methoden

  

Die Bibliothek RxJava verfügt über zahlreiche Methoden, die Instanzen generischer Schnittstellen als Parameter akzeptieren. Manchmal ist deren Signatur komplex. Hier sind einige Beispiele:


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)

Diese beiden Methoden verwenden zwei generische Typen T und R. Aber was bedeutet beispielsweise die Definition des Parameters der Methode „map“: Func1<? super T,? extends R> func ?

Informationen zu Generika finden Sie unter URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Einige der folgenden Informationen stammen aus dieser Quelle: URL. Generika wurden mit der Version 1.5 von Java eingeführt.

Stellen wir uns einen Webdienst vor, der Informationen verschiedener Typen bereitstellt. Der Webdienst kann manchmal bei der Bereitstellung dieser Informationen fehlschlagen und muss dann seinem Client einen Fehler melden. Die Antwort des Webdienstes lässt sich dann wie folgt standardisieren:


public class Response<T> {

    // ----------------- Eigenschaften
    // Status der Operation
    private int status;
    // etwaige Fehlermeldungen
    private List<String> messages;
    // der Antworttext
    private T body;
...
}
  • Zeile 1: Die Klasse [Response] wird durch den Typ T parametrisiert;
  • Zeile 9: Der Typ T ist der Typ des Antwortkörpers, der tatsächlich vom Client erwartet wird;
  • Zeile 5: ein Fehlercode, 0, wenn kein Fehler vorliegt;
  • Zeile 7: Meldungen, die den Fehler erklären, null, wenn kein Fehler vorliegt;

Auf der Client-Seite kann man dann schreiben:


Response<Product> product=getDataFromWebService(...) ;

oder


Response<List<Product>>=getDataFromWebService(...) ;

Der formale Typ T wird durch einen effektiven Typ ersetzt, hier [Product] oder [List<Product>]. Die generische Klasse [Response<T>] erweist sich hier als praktisch, da sie die Arbeit mit einer Standardantwort ermöglicht.

Um einen Typ [Response] mit dem Operator „new“ zu erstellen, schreibt man:


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

Seit Java-Version 1.8 lässt sich die obige Anweisung einfacher schreiben:


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

Es ist nun nicht mehr erforderlich, rechts vom Gleichheitszeichen den tatsächlichen Typ der generischen Parameter anzugeben: Es wird der formale Typ übernommen, der links vom Gleichheitszeichen angegeben wurde. Dies wird als Typinferenz bezeichnet: Der Compiler ist in der Lage, den effektiven Typ der generischen Parameter anhand des Kontexts selbst zu ermitteln. Diese Besonderheit wird häufig in Lambda-Funktionen genutzt, bei denen der effektive Typ der generischen Parameter einer Methode oft weggelassen wird.

Auf der Client-Seite könnte die Methode [getDataFromWebService] nun folgende Signatur haben:


<T1,T2> Response<T1>  getDataFromWebService(String urlWebService, String httpMethod, T2 post)

Hier handelt es sich um eine generische Methode, die durch zwei Typen parametrisiert ist: T1 und T2:

  • T1 ist der Typ des erwarteten Antwortkörpers;
  • T2 ist der Typ des gesendeten Werts für eine Operation POST, null für eine Operation GET;
  • urlWebService: ist der URL des Webdienstes;
  • httpMethod: ist die Methode HTTP, GET oder POST, die zur Abfrage dieses URL verwendet werden soll;

Auf der Client-Seite könnte der folgende Aufruf erfolgen:


Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;

In diesem Fall gilt: T1 = Product und T2 = Long.

Mit Java 8 kann man die Typinferenz nutzen und schreibt dann einfacher:


Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;

Hier ist ein weiterer möglicher Aufruf:


Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;

In diesem Fall gilt: T1 = List<Product> und T2 = Long[].

Die generische Klasse oder Methode kann Einschränkungen für ihre generischen Parameter festlegen. Kehren wir zur Definition der Methode [map] von RxJava zurück:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)

Die Methode akzeptiert zwei Parametertypen, T und R. Der Typ [Func1] ist eine generische Schnittstelle:

 

Func1 definiert eine funktionale Schnittstelle, d. h. eine Schnittstelle mit nur einer einzigen Methode. Funktionale Schnittstellen bilden die Grundlage für die Lambda-Funktionen in Java 8. Hier ist die Methode der Schnittstelle wie folgt definiert:

R call(T t)

T ist also der Typ des an [call] übergebenen Parameters und R der Typ des erhaltenen Ergebnisses. Kehren wir zur Definition der Methode [map] zurück:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)
  • Die Methode [map] erwartet einen einzigen Parameter vom Typ [Func1<? super T,? extends R>] und gibt einen Typ [Observable<R>] zurück;
  • ? super T: Der an die Methode [Func1.call] übergebene Parameter muss vom Typ T oder ein übergeordneter Typ von T sein;
  • ? extends R: Der Typ des von der Methode [call] zurückgegebenen Ergebnisses muss vom Typ R oder davon abgeleitet sein, wenn R eine Klasse ist, oder R implementieren, wenn R eine Schnittstelle ist;

Um die beiden vorstehenden Einschränkungen besser zu verstehen, betrachten wir einige Beispiele aus der Referenz [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"); // Fehler: Dies ist immer noch ein String!
    }
}
  • Zeile 3: Die Klasse [Box] wird durch den Typ T parametrisiert, der dem Typ des Feldes in Zeile 5 entspricht;
  • Zeile 15: Die Methode [inspect] akzeptiert einen Parameter vom Typ U. Auch eine Methode kann generische Parameter haben. Sie wird dann wie folgt deklariert:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

Hier wird U durch <U> als generischer Typ der Methode deklariert, jedoch mit der Einschränkung <U extends Number>, d. h., U muss die Klasse [Number] erweitern. Aufgrund dieser Einschränkung meldet der Compiler in Zeile 25 einen Fehler;

  • Zeilen 23–24: Aufrufe der Methode [inspect] mit von [Number] abgeleiteten Typen;

Es ergeben sich folgende Ergebnisse:

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

Hinweis: Um die Klasse in IntelliJ auszuführen, gehen Sie wie folgt vor: [1, 2]:

 

Betrachten wir nun das folgende Beispiel:


    public void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 3; i++) {
            list.add(i);
        }
}

Die Methode [addNumbers] akzeptiert als Parameter einen Typ List<T>, wobei T eine übergeordnete Klasse der Klasse [Integer] ist. Aufgrund dieser Einschränkung können die Ganzzahlen int 1 bis 3 zur Liste hinzugefügt werden (Zeilen 2–4), und die Methode könnte wie folgt aufgerufen werden:


    List<Number> numbers=new ArrayList<>();
    // Hinzufügen eines Double-Werts <-- Double
    numbers.add(7.8);
    // Hinzufügen von Long Number <-- Long
    numbers.add(1L);
    // Hinzufügen von List<Number> Number <-- Integer
    addNumbers(numbers);
    // Anzeige von List<Number>
    for(Number number : numbers){
      System.out.println(number.getClass().getName());
}

Man erhält dann das folgende Ergebnis:

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

Um die Klasse Box&lt;T&gt; zu erweitern, schreibt man:


class OtherBox<T> extends Box<T> {

}

Die untergeordnete Klasse kann ihrerseits neue generische Parameter einführen:


class AnotherBox<T, T1> extends Box<T> {
    void consumer(T1 t1) {
        System.out.printf("%s%n", t1);
    }
}

Auch eine Schnittstelle kann generische Parameter verwenden:


interface I<T> {
    void doSomethingWith(T t);
}

Zur Implementierung wird die folgende Syntax verwendet:


class A<T> implements I<T> {

    @Override
    public void doSomethingWith(T t) {
        System.out.printf("%s%n", t);
    }

}

Wir wissen nun genug über Generika, um uns mit Lambda-Funktionen zu befassen.