Skip to content

3. De signaturen van generieke klassen en methoden

  

De bibliotheek RxJava bevat talrijke methoden die instanties van generieke interfaces als parameters accepteren. Soms is de signatuur hiervan complex. Hier volgen enkele voorbeelden:


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)

Deze twee methoden maken gebruik van twee generieke typen T en R. Maar wat betekent bijvoorbeeld de definitie van de parameter van de methode map: Func1<? super T,? extends R> func ?

Informatie over generieke typen is te vinden in de URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Sommige van de onderstaande informatie is afkomstig uit deze URL. Generieke typen zijn geïntroduceerd in versie 1.5 van Java.

Stel je een webservice voor die verschillende soorten informatie levert. Het kan voorkomen dat de webservice er niet in slaagt deze informatie te leveren en dan een foutmelding aan de klant moet geven. Het antwoord van de webservice kan dan als volgt worden gestandaardiseerd:


public class Response<T> {

    // ----------------- eigenschappen
    // status van de bewerking
    private int status;
    // eventuele foutmeldingen
    private List<String> messages;
    // de inhoud van het antwoord
    private T body;
...
}
  • regel 1: de klasse [Response] wordt gedefinieerd door het type T;
  • regel 9: het type T is het type van de hoofdtekst van het antwoord, wat daadwerkelijk door de client wordt verwacht;
  • regel 5: een foutcode, 0 als er geen fout is;
  • regel 7: berichten die de fout toelichten, null als er geen fout is;

Aan de kant van de client kunnen we dan het volgende schrijven:


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

of


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

Het formele type T wordt vervangen door een effectief type, in dit geval [Product] of [List<Product>]. De generieke klasse [Response<T>] komt hier goed van pas, omdat deze het mogelijk maakt om met een standaardantwoord te werken.

Om een type [Response] aan te maken met de operator new, schrijven we:


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

Vanaf Java-versie 1.8 kan de bovenstaande instructie eenvoudiger worden geschreven:


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

Het is nu niet meer nodig om rechts van het =-teken het daadwerkelijke type van de generieke parameters te specificeren: het formele type dat links van het =-teken is opgegeven, wordt overgenomen. Dit wordt type-inferentie genoemd: de compiler is in staat om zelf het effectieve type van de generieke parameters te bepalen op basis van de context. Deze eigenschap wordt veelvuldig gebruikt in lambda-functies, waar het effectieve type van de generieke parameters van een methode vaak wordt weggelaten.

Aan de clientzijde zou de methode [getDataFromWebService] nu de volgende signatuur kunnen hebben:


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

Dit is een generieke methode met twee parameters van het type T1 en T2:

  • T1 is het type van de inhoud van het verwachte antwoord;
  • T2 is het type van de verzonden waarde voor een bewerking POST, null voor een bewerking GET;
  • urlWebService: is de URL van de webservice;
  • httpMethod: is de methode HTTP, GET of POST die moet worden gebruikt om deze URL op te vragen;

Aan de clientzijde kan de volgende aanroep worden gedaan:


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

In dit geval geldt T1=Product en T2=Long.

Met Java 8 kunnen we gebruikmaken van type-inferentie en schrijven we eenvoudiger:


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

Hier is nog een andere mogelijke aanroep:


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

In dit geval geldt T1=List<Product> en T2=Long[].

De generieke klasse of methode kan beperkingen opleggen aan hun generieke parameters. Laten we teruggaan naar de definitie van de methode [map] van RxJava:


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

De methode accepteert twee typen parameters, T en R. Het type [Func1] is een generieke interface:

 

Func1 definieert een functionele interface, d.w.z. een interface met slechts één methode. Functionele interfaces vormen de basis van de lambda-functies in Java 8. Hier wordt de methode van de interface als volgt gedefinieerd:

R call(T t)

T is dus het type van de parameter die wordt doorgegeven aan [call] en R het type van het verkregen resultaat. Laten we teruggaan naar de definitie van de methode [map]:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)
  • de methode [map] verwacht één parameter van het type [Func1<? super T,? extends R>] en retourneert een type [Observable<R>];
  • ? super T: de parameter die aan de methode [Func1.call] wordt doorgegeven, moet van het type T of een bovenliggend type van T zijn;
  • ? extends R: het type van het resultaat dat door de methode [call] wordt geretourneerd, moet van het type R zijn of daarvan afgeleid zijn als R een klasse is, of R implementeren als R een interface is;

Om de twee voorgaande beperkingen beter te begrijpen, nemen we enkele voorbeelden uit de referentie [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"); // fout: dit is nog steeds een String!
    }
}
  • regel 3: de klasse [Box] wordt gedefinieerd door het type T, dat het type is van het veld op regel 5;
  • regel 15: de methode [inspect] accepteert een parameter van het type U. Een methode kan ook generieke parameters hebben. Deze wordt dan als volgt gedeclareerd:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

Hier wordt U gedeclareerd als generiek type van de methode via <U>, maar met de beperking <U extends Number>, d.w.z. dat U de klasse [Number] moet uitbreiden. Vanwege deze beperking meldt de compiler een fout op regel 25;

  • regels 23-24: aanroepen van de methode [inspect] met typen die zijn afgeleid van [Number];

Dit levert de volgende resultaten op:

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

Opmerking: om de klasse in IntelliJ uit te voeren, gaat u als volgt te werk voor [1, 2]:

 

Laten we nu eens kijken naar het volgende voorbeeld:


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

De methode [addNumbers] accepteert als parameter een type List<T>, waarbij T een bovenliggende klasse is van de klasse [Integer]. Vanwege deze beperking kunnen de gehele getallen int 1 tot en met 3 aan de lijst worden toegevoegd (regels 2-4) en zou de methode als volgt kunnen worden aangeroepen:


    List<Number> numbers=new ArrayList<>();
    // toevoeging van een Double-getal <-- Double
    numbers.add(7.8);
    // toevoeging van een Long-getal <-- Long
    numbers.add(1L);
    // toevoeging van List<Number> Number <-- Integer
    addNumbers(numbers);
    // weergave van List<Number>
    for(Number number : numbers){
      System.out.println(number.getClass().getName());
}

Dit levert dan het volgende resultaat op:

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

Om de klasse Box&lt;T&gt; uit te breiden, schrijven we:


class OtherBox<T> extends Box<T> {

}

De afgeleide klasse kan zelf nieuwe generieke parameters introduceren:


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

Ook een interface kan generieke parameters gebruiken:


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

Om deze te implementeren, gebruikt men de volgende syntaxis:


class A<T> implements I<T> {

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

}

We weten nu genoeg over generieke typen om lambda-functies te bespreken.