Permettent de paramétrer classes, interfaces et méthodes par des types, comme les paramètres habituels des méthodes permettent de paramétrer par des valeurs.
Beaucoup utilisés pour les collections.
La syntaxe utilise
< >
Par exemple
java.util.List
Sans type paramétré, une
List
gère une collection de java.lang.object
.
public interface List extends Collection{ ... }Utilisation :
List l = new ArrayList(); l.add("toto");Les méthodes pour gérer des éléments (
add()
, get()
...) manipulent des Object
.
Avec les generics, on peut paramétrer par un type la création d'une liste :
public interface List<E> extends Collection<E>{ ... }Permet de créer une liste dont les éléments sont de type
E
.
E
représente un type non primitif.
List<String> l = new ArrayList<String>(); List<Integer> l = new ArrayList<Integer>();Intérêts :
-
Simplifier le code.
List l = new ArrayList(); l.add("hello"); String s = (String) l.get(0); // cast obligatoire
List<String> l = new ArrayList<String>(); l.add("hello"); String s = l.get(0); // pas besoin de cast
-
Faire apparaître des bugs à la compilation en renforçant le typage.
List l = new ArrayList(); l.add("hello"); l.add(Integer.valueOf(3)); String s1 = (String) l.get(0); String s2 = (String) l.get(1); // erreur à l'exécution
(code dans TestGenerics1.java)List<String> l = new ArrayList<String>(); l.add("hello"); l.add(Integer.valueOf(3)); // erreur à la compilation
(code dans TestGenerics2.java) - Ecrire des structures génériques.
Generic types
(= types génériques)Soit une classe
Box
qui encapsule un objet, avec deux méthodes, get()
et set()
.
public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } }Dans la version de la classe paramétrée par un type,
T
peut être utilisé dans tout le code de la classe :
/** * Generic version of the Box class. * @param <T> the type of the value being boxed */ public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } }A l'utilisation :
Box<String> b = new Box<String>(); b.set("hello"); String s = b.get();Conventions pour les noms de types (utilisées dans l'API java) :
E - Element K - Key N - Number R - Return T - Type V - ValueExemples très nombreux dans l'api java :
java.util.Collection<E> java.util.Map<K,V> java.util.stream.Stream<T> static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next) <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
Inférence de type
Depuis java 7, il est possible de ne pas spécifier le type lorsqu'il peut être inféré (deviné) par le compilateur :Box<String> b = new Box<String>;peut être remplacé par :
Box<String> b = new Box<>;
Paramétrer avec plusieurs types
La syntaxe générale est<T1, T2, ..., Tn>
(les Ti
représentent des types).
public interface Pair<K, V> { public K getKey(); public V getValue(); } public class OrderedPair<K, V> implements Pair<K, V> { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } // en utilisant l'inférence de type : Pair<String, Integer> p1 = new OrderedPair<>("Even", 8); // autoboxing de 8 vers un Integer Pair<String, String> p2 = new OrderedPair<>("hello", "world");Voir par exemple dans l'API
java.util.Map
Un type paramétré peut être lui-même paramétré :
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
Raw types
Si on a :public class Box<T> { public void set(T t) { ... } // ... }A l'utilisation, on fait normalement :
Box<Integer> intBox = new Box<>();Mais on peut faire aussi :
Box rawBox = new Box();Dans ce cas,
T
vaut implicitement Object
, et on parle du raw type (type brut) de Box<>();
Attention : un type non générique n'est pas un raw type.
L'utilisation de raw types génère des warnings à la compilation.
public class TestRawType1{ public static void main(String[] args){ List l = new ArrayList(); l.add("hello"); // ici warning compilation l.add(Integer.valueOf(3)); // ici warning compilation String s1 = (String) l.get(0); String s2 = (String) l.get(1); } }
javac TestRawType1.java
Note: Main1.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
javac -Xlint:unchecked TestRawType1.java
TestRawType1.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type List l.add("hello"); ^ where E is a type-variable: E extends Object declared in interface List(code dans TestRawType1.java)
Pour désactiver les warnings, utiliser l'annotation
@SuppressWarnings("unchecked")
@SuppressWarnings("unchecked") public class TestRawType2{ public static void main(String[] args){ // ... } }(code dans TestRawType2.java)
Méthodes génériques
Méthode générique = méthode qui introduit ses propres types paramétrés, mais dont la portée est limitée à la méthode.Meme syntaxe que pour la déclaration de types paramétrés.
Peut se trouver dans des méthodes statiques, d'instance ou des constructeurs.
Pour les méthodes statiques, les types doivent se trouver avant le type de retour.
class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } } class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }Utilisation :
public class TestGenericMethods1{ public static void main(String[] args){ Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.<Integer, String>compare(p1, p2); // Mais le compilateur est capable d'inférer le type, donc on peut faire : boolean same = Util.compare(p1, p2); } }(code dans TestGenericMethods1.java)
Bien voir la différence : dans la classe
Pair<K, V>
, les types K
et V
sont définis au niveau de la classe, donc sont connus par tout le code de cette classe.
La classe
Util
ne définit pas de type paramétré. Pour permettre à compare()
d'utiliser des types paramétrés, on a besoin de paramétrer la méthode.
Bounded type parameters
( = types paramétrés limités)extends
signifie ici "extends" ou "implements" (même sémantique que l'annotation @Override
).
public class BoundedTypeInGenericMethod{ public static void main(String[] args) { Box<Integer> ib = new Box<>(); ib.set(Integer.valueOf(10)); // ib.inspect("some text"); // erreur de compilation ib.inspect(3.2); // autoboxing vers Double } } 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()); } }(code dans BoundedTypeInGenericMethod.java)
Intéressant : permet d'utiliser les méthodes du type limité :
public class NumberBox<T extends Integer> { private T n; public NumberBox(T n) { this.n = n; } public boolean isEven() { return n.intValue() % 2 == 0; } }Dans
isEven()
, n
est un objet de type Integer (ou d'une sous-classe), donc les méthodes de la classe Integer
sont utilisables.
Exemple d'utilité de la clause
extends
:
public static <T> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e > elem) // erreur de compilation ++count; return count; }Erreur de compilation car l'opérateur
>
ne s'applique pas aux objets en général, uniquement à certains types primitifs tels que int
.
Pour pouvoir comparer des objets, on utilise l'interface
java.lang.Comparable
.
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e.compareTo(elem) > 0) ++count; return count; }(noter au passage que
extends
ici signifie implements
)
Limites multiples
On peut aussi spécifier des limites multiples :<T extends B1 & B2 & B3>
T
doit être un sous-type de tous les types séparés par des &
.
Cette syntaxe n'a de sens que pour spécifier des
implements
multiples.
Si un des
Bi
est une classe, doit être mis en premier.
Héritage
En java, on peut assigner des objets d'un type à des objets d'un autre type, si ces types sont compatibles :Object someObject = new Object(); Integer someInteger = Integer.valueOf(10); someObject = someInteger; // OKOK parceque
Integer
est un Object
(qui peut le plus peut le moins).
Mais
Integer
est aussi un Number
, donc on peut faire :
public void someMethod(Number n) { ... } someMethod(Integer.valueOf(10)); // OK someMethod(Double.valueOf(10.1)); // OKMarche aussi avec les generics :
Box<Number> box = new Box<>(); box.add(Integer.valueOf(10)); // OK box.add(Double.valueOf(10.1)); // OKMais ATTENTION, si on a la méthode :
public void boxTest(Box<Number> n) { ... }Est-ce qu'on peut passer à cette méthode des
Box<Integer>
ou des Box<Double>
?
NON car
Sous-classage
On peut sous-classer une classe ou une interface générique avec les clausesextends
et implements
.
Exemple pour les sous-classes de
java.util.Collection
List<E> extends Collection<E> ArrayList<E> implements List<E>Donc
ArrayList<String>
est un sous-type de List<String>
, qui est un sous-type de Collection<String>
Si on définit notre propre interface pour gérer des listes :
(une liste où chaque élément est associé à un élément de type
P
)
interface PayloadList<E,P> extends List<E> { void setPayload(int index, P val); }(PayloadList ~ liste enrichie).
Tous ces paramétrages de
PayloadList
sont sous-classes de List<String>
:
PayloadList<String,String> PayloadList<String,Integer> PayloadList<String,Exception>
Wildcard
(wildcard = joker, comme*
dans une expression régulière ou %
en sql)
Ici, le caractère utilisé est
?
?
représente un type inconnu à la compilation.
Upper bounded wildcard
(= wildcard avec limite supérieure)Alors que
List<Number>repésente uniquement une liste de type
Number
,
List<? extends Number>représente une liste de
Number
ou de ses sous-classes, comme List<Integer>
ou List<Double>
Donc
List<? extends Number>
est moins restrictif que List<Number>
.
NOTE : ici, extends est pris au sens de
extends
ou implements
.
Très utilisé dans l'API java, par exemple dans
Interface java.util.Map<K,V>
void putAll(Map<? extends K,? extends V> m)Dans certains cas, l'usage du wildcard peut etre évité :
import java.util.List; import java.util.Arrays; public class TestWildcard{ public static void main(String[] args){ List<Integer> li = Arrays.asList(1, 2, 3); System.out.println("sumOfList1 = " + sumOfList1(li)); System.out.println("sumOfList2 = " + sumOfList2(li)); } public static double sumOfList1(List<? extends Number> list) { double s = 0.0; for (Number n : list) s += n.doubleValue(); return s; } public static <T extends Number> double sumOfList2(List<T> list) { double s = 0.0; for (Number n : list) s += n.doubleValue(); return s; } }(code dans TestWildcard.java)
On verra plus loin que si ces 2 méthodes donnent le même résultat, elles ne sont pas strictement équivalentes
car
<? extends Number>
et <T extends Number>
sont de types différents, dans deux hiérarchies d'héritage différentes.
Unbounded wildcard
(= wildcard sans limite) ; par exList<?>Si on veut afficher une liste de n'importe quel type,
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println(); }ne permet pas d'afficher tous les types, par ex
List<Integer>
ou List<Double>
, qui dérivent de Object
mais pas de List<Object>
.
Mais
public static void printList(List<?> list) { ... }accepte une liste de n'importe quel type, comme
List<Object>
, List<Integer>
ou List<Double>
.
INCERTITUDE
Pourtant cette syntaxe fonctionne aussi :
printList()
est l'exemple donné par Oracle pour justifier le besoin de unbounded wildcard.
Pourtant cette syntaxe fonctionne aussi :
public static void printList(List<? extends Object> list) { ... }(code dans UnboundedOracle.java)
<?>
est peut-être seulement un raccourci syntaxique de <? extends Object>
.
Lower bounded wildcard
(= wildcard avec limite inférieure)List<? super Integer>permet de travailler avec des listes dont le type paramétré est une superclasse de
Integer
Par exemple, pour écrire une méthode qui travaille sur des listes d'entiers et de tous ses supertypes (
Number
, Object
) :
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } }
List<? super Integer>
est moins restrictif que List<Number>
.
Wildcard et héritage
Rappel :class A { ... } class B extends A { ... } // on peut tout à fait écrire : B b = new B(); A a = b; // qui peut le plus (B) peut le moins (A) // mais pas : List<B> lb = new ArrayList<>(); List<A> la = lb; // erreur de compilationEn effet, on a vu que
List<B>
n'est pas un sous-type de List<A>
.
Les wildcards permettent de créer des relations d'héritage.
Déjà, on a la relation suivante : Plus généralement, si
B
est un sous-type de A
, < ? extends B>
est un sous-type de < ? extends A>
.
Par exemple : Donc dans l'exemple précédent, si on ne peut pas faire :
List<B> lb = new ArrayList<>(); List<A> la = lb;on peut faire :
List<? extends B> lb = new ArrayList<>(); List<? extends A> la = lb;
Exercice : Personnes
Exercice : Erreur de schéma
Exercice : Compare
Type erasure
A savoir : tous les types sont traduits à la compilation ; le code compilé se retrouve sans type générique.La traduction n'est pas triviale, par exemple des casts sont rajoutés, des méthodes supplémentaires sont générées dans le cas d'utilisation de
extends
.