3. Les signatures des classes et méthodes génériques
![]() |
La bibliothèque RxJava possède de nombreuses méthodes acceptant pour paramètres des instances d'interfaces génériques. Parfois, la signature de celles-ci est complexe. En voici quelques exemples :
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)
Ces deux méthodes utilisent deux types génériques T et R. Mais que veut dire par exemple la définition du paramètre de la méthode map : Func1<? super T,? extends R> func ?
Les informations sur les génériques peuvent être trouvées à l'URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Certaines informations ci-dessous prennent leur source à cette URL. Les génériques sont apparus avec la version 1.5 de Java.
Imaginons un service web délivrant des informations de différents types. Le service web peut parfois échouer à délivrer cette information et doit alors signaler une erreur à son client. On peut alors standardiser la réponse du service web sous la forme suivante :
public class Response<T> {
// ----------------- propriétés
// statut de l'opération
private int status;
// les éventuels messages d'erreur
private List<String> messages;
// le corps de la réponse
private T body;
...
}
- ligne 1 : la classe [Response] est paramétrée par le type T ;
- ligne 9 : le type T est le type du corps de la réponse, ce qui est réellement attendu par le client ;
- ligne 5 : un code d'erreur, 0 si pas d'erreur ;
- ligne 7 : des messages expliquant l'erreur, null si pas d'erreur ;
Côté client, on pourra alors écrire :
Response<Product> product=getDataFromWebService(...) ;
ou
Response<List<Product>>=getDataFromWebService(...) ;
Le type formel T est remplacé par un type effectif, ici [Product] ou [List<Product>]. La classe générique [Response<T>] se montre ici pratique car elle permet de travailler avec une réponse standard.
Pour créer un type [Response] avec l'opérateur new, on écrira :
Response<List<Product>> response=new Response<List<Product>> (...) ;
Depuis la version 1.8 de Java, l'instruction précédente peut être écrite plus simplement :
Response<List<Product>> response=new Response<> (...) ;
Il n'y a désormais plus lieu de préciser à droite du signe =, le type effectif des paramètres génériques : ce sera le type formel précisé à gauche du signe = qui sera repris. On appelle cela de l'inférence de type : le compilateur est capable de trouver lui-même le type effectif des paramètres génériques d'après le contexte. Cette particularité est abondamment utilisée dans les fonctions lambda où le type effectif des paramètres génériques d'une méthode est souvent omis.
Maintenant côté client, la méthode [getDataFromWebService] pourrait avoir la signature suivante :
<T1,T2> Response<T1> getDataFromWebService(String urlWebService, String httpMethod, T2 post)
On a là une méthode générique paramétrée par deux types T1 et T2 :
- T1 est le type du corps de la réponse attendue ;
- T2 est le type de la valeur postée pour une opération POST, null pour une opération GET ;
- urlWebService : est l'URL du service web ;
- httpMethod : est la méthode HTTP GET ou POST à utiliser pour interroger cette URL ;
Côté client, on pourra avoir l'appel suivant :
Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
Dans ce cas on aura T1=Product et T2=Long.
Avec Java 8, on peut utiliser l'inférence de type et on écrira plus simplement :
Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
Voici un autre appel possible :
Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;
Dans ce cas on aura T1=List<Product> et T2=Long[].
La classe ou la méthode générique peuvent mettre des contraintes sur leurs paramètres génériques. Revenons sur la définition de la méthode [map] de RxJava :
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
La méthode admet deux types paramètres, T et R. Le type [Func1] est une interface générique :
![]() |
Func1 définit une interface fonctionnelle, ç-à-d une interface n'ayant qu'une unique méthode. Les interfaces fonctionnelles sont à la base des fonctions lambda de Java 8. Ici, la méthode de l'interface est définie comme :
Donc T est le type du paramètre passé à [call] et R le type du résultat obtenu. Revenons à la définition de la méthode [map] :
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
- la méthode [map] attend un unique paramètre de type [Func1<? super T,? extends R>] et rend un type [Observable<R>] ;
- ? super T : le paramètre passé à la méthode [Func1.call] doit être un type T ou parent de T ;
- ? extends R : le type du résultat rendu par la méthode [call] doit être de type R ou dérivé si R est une classe ou implémenter R si R est une interface ;
Pour mieux comprendre les deux contraintes précédentes, prenons des exemples tirés de la référence [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"); // error: this is still String!
}
}
- ligne 3 : la classe [Box] est paramétrée par le type T qui est le type du champ de la ligne 5 ;
- ligne 15 : la méthode [inspect] admet un paramètre de type U. Une méthode peut elle aussi avoir des paramètres génériques. Elle est alors déclarée de la façon suivante :
Ici, U est déclaré type générique de la méthode par <U> mais avec une contrainte <U extends Number>, ç-à-d que U doit étendre la classe [Number]. A cause de cette contrainte, le compilateur signale une erreur ligne 25 ;
- lignes 23-24 : appels de la méthode [inspect] avec des types dérivés de [Number] ;
On obtient les résultats suivants :
Note : pour exécuter la classe dans Intellij, procédez comme suit [1, 2] :
![]() |
Maintenant, considérons l'exemple suivant :
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
La méthode [addNumbers] admet pour paramètre un type List<T> où T est une classe parent de la classe [Integer]. A cause de cette restriction, les entiers int 1 à 3 peuvent être ajoutés à la liste (lignes 2-4) et la méthode pourrait être appelée de la façon suivante :
List<Number> numbers=new ArrayList<>();
// ajout double Number <-- Double
numbers.add(7.8);
// ajout Long Number <-- Long
numbers.add(1L);
// ajout List<Number> Number <-- Integer
addNumbers(numbers);
// affichagede List<Number>
for(Number number : numbers){
System.out.println(number.getClass().getName());
}
On obtient alors le résultat suivant :
Pour étendre la classe Box<T>, on écrira :
class OtherBox<T> extends Box<T> {
}
La classe fille peut elle-même introduire de nouveaux paramètres génériques :
class AnotherBox<T, T1> extends Box<T> {
void consumer(T1 t1) {
System.out.printf("%s%n", t1);
}
}
Une interface peut utiliser elle aussi des paramètres génériques :
interface I<T> {
void doSomethingWith(T t);
}
Pour l'implémenter, on utilise la syntaxe suivante :
class A<T> implements I<T> {
@Override
public void doSomethingWith(T t) {
System.out.printf("%s%n", t);
}
}
On en sait maintenant suffisamment sur les génériques pour aborder les fonctions lambdas.


