Après plus de cinq ans dattente depuis la première proposition,
les generics vont finalement être intégrés au langage
Java, dans le J2SE 5.0. Le chemin pour arriver à ladoption de
cette technologie pour le moins controversée mais sans doute
essentielle fut long et douloureux.
Le manque des types génériques dans le langage Java est sujet à
polémique quasiment depuis la sortie de ce langage de programmation.
En effet, cette fonctionnalité est une des extensions du langage
les plus demandées par la communauté des développeurs Java (JDC).
Elle arrive en seconde position pour les extensions du
langage et en septième dans le bug parade.
Ainsi nous comprenons mieux limportance que prend cet
ajout vis-à-vis du langage.
Le point de départ fut la conférence donnée par Gilad Bracha,
théologiste chez Sun et aujourdhui le leader technique de la
JSR 014, « Add Generic Types to the Java Programming Language ».
Limplémentation retenue pour les generics en Java est basée sur celle de GJ (Generics Java),
une extension du langage Java, développée par Philip Wadler, Martin Odersky, Gilad Bracha,
et Dave Stoutamire.
Nous allons voir dans ce chapitre ce que sont les generics
et ce quils permettent.
Le principe des generics
Pour les personnes familières avec les templates en
C++, les generics prennent tout leurs sens, bien que des
différences fondamentales existent.
Par type générique, on entend aussi polymorphisme paramétrique de type.
De manière concise, on peut dire que les types génériques
permettent de développer un comportement unique pour des
types polymorphes.
En dautres termes, les generics permettent de
sabstraire du typage des objets lors de la conception et
donc de définir des comportements communs quel que soit le
type des objets manipulés.
Le plus simple pour comprendre les generics est
danalyser un exemple type :
List integerList = new ArrayList();
integerList.add(new Integer(1));
Integer i = (Integer)integerList.get(0);
Dans cet exemple, nous remarquons que nous sommes obligés de
transtyper explicitement lobjet que lon récupère de la
collection, car la seule chose dont nous sommes sûrs, cest
que litérateur retourne un Object.
Ainsi, rien ne nous empêche dinsérer un objet de type
String dans la liste :
integerList.add(exemple); // (1)
Une telle construction, bien que fausse, ne déclenchera
aucune erreur lors de la compilation.
Cest seulement à lutilisation que le programme lèvera une
exception de type java.lang.ClassCastException.
Avec Tiger, il est possible, via une syntaxe particulière,
de spécifier explicitement et avant construction, quel sera
le type des objets contenus dans la collection :
List integerList<Integer> = new ArrayList<Integer>();
integerList.add(new Integer(1));
Integer i = integerList.get(0);
La syntaxe <Integer> spécifie que le type des
objets utilisés avec cette collection est Integer.
Ainsi, lexemple précédent (1) fera échouer la compilation
car les generics introduisent une vérification de
type statique, c'est-à-dire durant la compilation.
En fait, dans Tiger, les collections sont désormais
implémentées en utilisant les types génériques.
Quels sont les avantages des generics? Cest ce que
nous allons voir dans le paragraphe suivant.
Pourquoi utiliser les generics ?
Le premier avantage des generics réside dans la
suppression du contrôle de type à lexécution.
En effet, étant donné que les éventuelles erreurs de
transtypage sont levées à la compilation, il nest plus
nécessaire de contrôler le type à lexécution.
Ainsi, le code gagne en sécurité et le temps de maintenance
éventuel en est très fortement réduit.
Dautre part, il n'y a pas de code supplémentaire à écrire (comme pour
les templates C++) pour introduire les generics.
La manière de développer ne change donc pas, si ce nest des
différences mineures dans la syntaxe (que nous verrons dans
le paragraphe suivant).
Un avantage visible directement est une meilleure lisibilité
et une plus grande robustesse du code.
Un autre aspect moins frappant mais tout aussi important,
est la possibilité quoffrent les types génériques de
factoriser des comportements.
Les développeurs passent moins de temps à refaire sans cesse
la même chose pour des types différents quils doivent
manipuler et peuvent ainsi se concentrer sur la logique
fonctionnelle de leurs classes.
Le code en est dautant plus concis et moins éparpillé et
gagne ainsi en cohérence, en taille et en qualité.
La syntaxe des generics en Java
Nous allons fixer dans ce chapitre un certain nombre
dappellations et de notations.
Comme nous lavons déjà vu dans le premier exemple, les
premiers symboles nouveaux avec les types génériques sont
les chevrons < et >.
Ils permettent de définir les paramètres de types formels
qui peuvent être utilisés dans toutes les déclarations
génériques, et en particulier en lieu et place des types
ordinaires (bien quil y ait dimportantes restrictions).
A linvocation de lobjet, toutes les occurrences du
paramètre de type formel sont remplacées par le type défini.
T est ici le paramètre de type formel de linterface
List.
Jinsiste bien sur le fait que T est un type, et pas
une valeur. Nous verrons pourquoi dans le chapitre suivant.
Dautre part, une convention de nommage semble être acceptée
par la communauté pour la notation des paramètres de type
formel.
Elle recommande lutilisation dun caractère unique en
majuscule comme E (Element) ou T
(Type).
Lautre symbole important pour les generics est le point
dinterrogation (?), que lon appelle aussi wildcard
ou inconnue.
Ainsi nous aurions pu écrire :
List unKnownList<?> = new ArrayList<String>();
Il permet de définir la variance dun type générique.
Il permet de spécifier que nimporte quel type peut convenir
comme élément de la liste.
Nous reviendrons plus loin sur les différentes possibilités
quoffre le wildcard.
Les generics ne sont pas des templates !
Nous lavions déjà évoqué dans lintroduction, les
generics ont des différences majeures avec les
templates tels que nous les connaissons en C++ par
exemple.
Nous avons vu que les occurrences dun paramètre de type
formel étaient remplacées à linvocation.
A la lecture de ces mots, il est très facile dextrapoler le
code précédent dune List qui aurait pour type String
par exemple :
Ce mécanisme ne doit pas aller sans rappeler aux développeurs
C++ les templates.
Et bien cette extrapolation est fausse, et cest en
cela que les templates et les generics sont
différent.
En effet, il ny a pas de multiples copies du code, en
fonction du type, ni en mémoire, ni dans les sources, ni
dans le bytecode généré.
Un generics est compilé une fois pour toute en une
unique classe comme une classe ou une interface classique et
linstance de cette classe est partagée entre toutes les
invocations :
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
boolean jvmIdentity = (l1.getClass() == l2.getClass());
Ainsi, la valeur booléenne jvmIdentity vaut true.
Les paramètres de type formel sont tout simplement remplacés
à linvocation comme les valeurs formelles le sont dans des
classes ordinaires, avec la valeur courante du type donnée
dans la construction du code (resp. la valeur donnée).
Les generics et l'héritage
Lhéritage avec les generics est une des choses les
plus ardues à comprendre, car elle va à contresens des
réflexions a priori.
En effet, si une classe C hérite dune classe
C, et G est un generics de paramètre
T, alors il est faux de dire que
G<C> hérite de G<C>. Pourtant
cela semblerait être logique.
Voyons cela avec un exemple.
Supposons que nous disposions de la liste des habitants
dune ville et de la liste des clients dun magasin de cette
ville. Nous supposons aussi que la classe Customer
hérite de la classe Resident.
List<Customer> customers = new ArrayList<Customer>;
Si nous supposons que tous les clients habitent la ville, on
peut penser initialiser la liste des habitants comme ceci :
List<Resident> residents = customers;
Or cette initialisation est fausse; une liste de
Resident nest pas une liste de Customer. Nous
allons voir pourquoi cette initialisation pose problème.
Supposons que linitialisation de la liste des habitants se
soit poursuivie comme ceci, sans lerreur précédente :
residents.add(new Resident( ));
Si par la suite nous tentons de récupérer le premier client
de la liste des clients, nous faisons naturellement :
Customer c = customers.get(0);
Or dans cette instruction nous tentons dassigner à une
instance de Customer une instance de Resident,
puisque nous avons ajouté un habitant à la liste des
habitants et que nous avons créé un alias entre les deux
listes.
Cela ne poserait pas de problème si la liste des habitants
était immuable. Or ce nest pas le cas. En réalité, ce qui
est passé à la liste des habitants est une copie de la
liste des résidents. Sinon, le service commercial du
magasin pourrait ajouter à sa liste de clients des personnes
qui ne sont pas clientes.
Cest pour cela que le compilateur de J2SE 5.0 lève une
erreur de compilation pour la seconde instruction.
Ceci est pour le moins restrictif, et pour pallier à cela,
Tiger comporte un mécanisme permettant de passer outre : il
sagit de lutilisation du wildcard.
La variance dans les generics
La variance désigne les limites de portée dun type générique.
Elle peut se définir comme les limites de la portée dun
type classique par les mots clé extends, super
et implements et sutilise de la même manière.
Pour combiner deux limites sur un même type, il faut
utiliser le caractère &.
Supposons que la mairie de la ville souhaite envoyer un
courrier à tous ses habitants qui sont clients du magasin.
Le service informatique de la mairie a une méthode permettant denvoyer
un courrier aux habitants de la ville.
Nous aurions une méthode qui ressemble à celle-ci :
Or nous avons vu dans le chapitre précédent quil est
impossible dassigner à une liste dhabitants une liste de
clients.
Quelle est alors lalternative ? Il est tout à fait normal
de vouloir (et pouvoir ...) faire ce type dopération. Pour
cela nous avons le wildcard (?).
Nous pouvons en effet modifier la méthode
mailResidents() afin quelle accepte nimporte quel
type dindividu :
Ainsi, comme Customer hérite de Resident,
lutilisation de la méthode devient valide dans notre
exemple précédent. Dans ce cas, la classe Resident
est dite la classe « upper bound » de la classe
Customer.
Cette construction a pourtant une limitation. Il est en
effet impossible de modifier la liste dans le corps de la
méthode, car nous ne connaissons pas le type effectif
(si ce nest que cest un type inconnu dont le supertype
est Resident). Toute tentative dajout délément nous
confronterait à un problème de compatibilité de type entre
le type effectif et le type de lobjet à ajouter.
Tiger encore une fois propose une possibilité pour
outrepasser ceci. Il sagit des méthodes génériques.
Les méthodes génériques
Les méthodes génériques sont des méthodes qui sont
paramétrées par des types génériques. Nous allons voir
comment celles-ci se mettent en uvre.
Supposons que le service du recensement fournisse à la
mairie une liste de personnes nouvellement installées dans
la commune. La mairie souhaite alors mettre à jour sa liste
dhabitants. La classe NewComer hérite de la classe
Resident.
La mairie dispose pour cela de la méthode suivante :
Nous avons bien pris soin de spécifier que les éléments de
la liste des résidents sont de tout type héritant de
Resident. Cependant comme nous venons de le dire, il
est impossible de modifier la liste passée en paramètre. La
ligne 6 provoque donc une erreur de compilation.
Les méthodes génériques arrivent ici à point nommé en
déduisant le type à assigner :
Jusquici nous avons toujours remplacé du code classique par
du code générique. Or, dans une application existante, il ne
sera sans doute pas possible de tout refaire en utilisant
les generics.
On peut isoler deux cas principaux :
Nous utilisons du code générique au sein dun code
ordinaire.
Nous ajoutons du code existant ordinaire dans du
code générique.
Dans le premier cas, le compilateur va lever des
avertissements sur les blocs de code qui pourraient poser
problème, comme par exemple des collections classiques qui
utilisent des collections génériques.
Dans le second cas, le compilateur va convertir le code
générique en code ordinaire. Ce processus est appelé
« erasure and translation ». Il supprime toutes les
informations génériques et transtype les objets dans un type
compatible (voire Object si aucun type nest
compatible).
Ce document est issu de http://www.developpez.com et reste la propriété
exclusive de son auteur [Lionel Roux]. La copie, modification et/ou
distribution, de tout ou partie de celui-ci, par quelque moyen que ce
soit, est soumise à l'obtention préalable de l'autorisation de l'auteur.
Tout manquement à cette licence donnerait systématiquement lieu à
des poursuites, en accord avec la loi sur l'économie numérique (LEN).