septembre
2006
Vous le savez sûrement, Java 5.0 a introduit un nouveau concept dans le langage : les Generics (également appelé types paramétrés en français). Au prime abord cela ressemble énormément aux Templates du C++ du fait de leurs syntaxes relativement proche et de leurs objectifs communs : écrire des méthodes/fonctions et classes génériques tout en permettant de les utiliser de manière spécialisé (c’est à dire en utilisant un type concret plutôt qu’un type abstrait).
Quels sont ces différences et pourquoi ?
Comment fonctionnent les Templates ?
Les Templates du C++ portent bien leurs noms, puisqu’il ne s’agit ni plus ni moins de modèles de code-source qui sont utilisés par le compilateur pour générer le code correspondant. Ainsi, lorsqu’on utilise une fonction ou une classe qui utilise les Templates, le compilateur générera automatiquement autant de fonctions/classes que nécessaire selon le nombre de type différent avec lequel elles sont utilisées (c’est pour cela que l’intégralité du code d’une classe template doit figuré dans le fichier d’entête .h).
Par exemple, si l’on utilise une classe List<T> avec deux types paramétrés tel que List<ClassA> et List<ClassB>, le compilateur générera deux classes distinctes en utilisant le même code et en remplaçant toutes les occurrences de T par le type utilisé (ClassA et ClassB dans ce cas). De manière imagé, c’est comme si le compilateur effectuait un copier/coller suivi d’un remplacement de terme (bien entendu il effectue en même temps plusieurs vérification sur la cohérence de l’ensemble, mais le principe est bien là).
Pour le développeur le gain est bien là, puisqu’il n’utilise qu’un seul et unique code générique. De plus, cela permet de conserver de très bonne performance puisque lors de l’exécution il n’y a plus aucun code générique du tout mais une répétition du même code fortement typé.
Toutefois, cela possède également un inconvénient : cela augmente la taille du code binaire généré, puisque si l’on utilise une classe template avec 40 types différents, cela revient en réalité à utiliser 40 classes différentes mais avec un code quasiment identique.
A noter que le même mécanisme général a été adopté pour les Generics de .NET 2.0 (même s’il doit y avoir quelques différences lorsque le code est exécuté via la machine virtuelle CLR).
Et les Generics de Java alors ?
En Java, les Generics ont pris une approche totalement différente : le code générique est conservé même à l’exécution. Le compilateur et la JVM ne généreront qu’un seul et unique code (bytecode et natif) qui sera utilisé par toutes les instances quel que soit le type paramétré utilisé. Et lorsqu’on utilise les Generics, il se charge de « caster » les paramètres et valeurs de retour des types paramétrés correctement, si bien qu’à l’exécution il ne reste aucune trace des Generics (on dit que le types paramétrés sont perdu à l’exécution). Bien entendu il vérifie en même temps la cohérence du code (c’est pourquoi cela a introduit un grand nombre de warning et d’erreur dans le processus de compilation). Mais si la compilation s’effectue sans aucun warning, l’utilisation des Generics garantit un code typesafe secure (c’est à dire qu’il ne devrait pas générer de ClassCastException à l’exécution).
Au final, cela revient exactement à la même chose que si le développeur avait utilisé une classe générique en castant les objets, sauf que c’est le compilateur qui prend en charge le tout et s’assure de la sécurité du code sans avoir d’impact significatif sur le code généré. C’est pourquoi les Generics de Java 5.0 sont parfois appelé « sucre syntaxique ».
Il en implique donc des performances moindres à cause de ce transtypage qui est toujours présent lors de l’exécution, mais ce coût est très faible et ne devrait pas vraiment poser de problème de performance notable. De plus cette perte est compensé par le fait qu’il y a moins de code a charger en mémoire du fait de l’absence de répétition du code, et mis à part dans des cas extrêmes d’utilisation, les différences doivent être minime…
Mais cela entraîne également d’autres restrictions par rapport à ses homologues C++ et C# :
- Il est impossible d’utiliser des types primitifs mais seulement des objets, étant donnée qu’il ne s’agit pas d’une recompilation mais d’une utilisation caché du transtypage.
- On ne peut pas connaître le type paramétré utilisé ni l’instancier puisqu’il est perdu lors de l’exécution (en clair
T.class
etnew T()
provoqueront des erreurs de compilation)
Même s’il existe bien sûr des alternatives plus ou moins pratique :
- On peut utiliser l’autoboxing/unboxing et le type wrapper correspondant pour utiliser des types primitifs avec les Generics.
- On peut utiliser un objet Class
en paramètre afin de déterminer le type exact qui est utilisé, et créer de nouvelle instance via la réflection et la méthode newInstance().
La raison du pourquoi du comment
Dès lors, on peut se demander quelle obscure raison a pousser le groupe d’expert de la JSR 14 à opter pour cette solution qui semble problématique à première vue. La réponse se trouve justement dans la description de cette JSR et de ses contraintes, qui en plus des habituelles contraintes de compatibilité ascendante rajoutait une notion de migration : il devait être possible de faire migrer des APIs existantes afin qu’elles utilisent les types paramétrés sans que cela ne pose de problème de compatibilité du code.
Ainsi, une grande partie de l’API standard a pu être mise à jour afin de bénéficier des avantages des Generics (en particulier à l’API des Collections) sans que cela ne pose aucun problème de compatibilité ascendante : une fois compilé le bytecode est quasiment le même (pas tout à fait dans le sens où il y a eu quelques modifications dans le langages et qu’il n’y a pas de compatibilité descendante).
Cette solution apporte également un autre avantage : les classes Generics peuvent être utilisé sans spécifier de type, et on se retrouve alors dans le même cas de figure qu’avec Java 1.4 et inférieur (avec toutefois quelques warnings en plus nous signalant de possible problème de type).
De même, on peut très bien utiliser des instances de classes paramétrés avec des méthodes/classes qui n’avait pas été conçus pour. Ainsi les nombreuses APIs non-standard ne sont pas devenu obsolète même si elle n’utilise pas (encore) les types paramétrés, et on peut très bien mélangé les deux au sein d’une même application. On peut donc utiliser conjointement du code compilé avec l’ancienne API et du code compilé avec la nouvelle API utilisant les Generics sans problème. Cela a l’avantage de permettre une transition en douceur…
A titre de comparaison, les Generics de .NET 2.0 ont impliquer la création d’une nouvelle API de Collections :
- L’espace de nom (l’équivalent de nos packages) System.Collections représente ainsi l’API de collections de .NET 1.0.
- L’espace de nom System.Collections.Generic représente quand à lui la nouvelle API de collection de .NET 2.0 qui utilise les Generics.
Ainsi par exemple, il existe deux interfaces équivalente à notre interface Collection
: System.Collections.ICollection
et System.Collections.Generic.ICollection
qui propose les mêmes fonctionnalités (si ce n’est l’utilisation des Generics dans la seconde) mais qui sont totalement incompatible (aucune relation entre les deux classes). Ainsi pour utiliser des collections utilisant les Generics avec .NET, il faut adapter tout son code pour utiliser la nouvelle API…
Cela a été possible grâce à la relative jeunesse du langage : en Java une telle situation aurait désormais sûrement provoqué un tollé de la part des développeurs étant donnée la grande quantité d’API existante qui utilise les collections, qu’il aurait fallut réécrire…
Et cela n’a pas que des désavantages
Cette vision différente des types paramétrés possède donc également ses propres avantages. En plus d’éviter la multiplication du bytecode, de la compatibilité parfaite avec les anciennes APIs et de la possibilité de faire migrer ces dernières sans dommage (d’ailleur il y a une grosse demande pour que Swing utilise les Generics dans le futur), cela apporte de nouvelles possibilitées :
- Il est ainsi possible de paramétré une méthode avec un type différent de la classe à laquelle elle appartient. Si je ne me trompe pas, en C++ seule les fonctions (indépendantes d’une classe) et les définitions de classes peuvent utiliser les Templates (et cela doit être également le cas en .NET), alors qu’en Java des méthodes de classe peuvent très bien utiliser les Generics indépendamment de leurs classes conteneurs (les méthodes peuvent être paramétrées différemment de la classe à laquelle elles appartiennent).
- La notion de wildcard
<?>
permet de limiter l’utilisation des méthodes d’une classe paramétré :<?> extends MaClasse
permet d’indiquer que la classe est paramétrée avec un type qui hérite ou implémenteMaClasse
(ou cette dernière). Or comme on ne connaît pas le type exact mais seulement la classe parente, on peut très bien utiliser les méthodes qui se contente de renvoyer un objet du type paramétré car il sera alors « downcaster » vers un référence de typeMaClasse
. Par contre il est impossible d’utiliser les méthodes qui attendent un type paramétré en paramètre puisqu’on ignore le type exact qui devrait être utilisé (et qu’il peut s’agir d’une classe fille). Dans le cas d’une Collection, cela reviendrait donc à ne pas pouvoir ajouter d’élément dans cette dernière.<?> super MaClasse
permet à l’inverse d’indiquer que la classe est paramétrée avec un type parent deMaClasse
(ou cette dernière) sans connaître son type exact. On se retrouve exactement dans le cas inverse : on peut utiliser les méthodes qui utilisent le type paramétré en paramètre (il sera alors downcaster) mais pas celles qui l’utilisent en type de retour puisqu’on ne peut pas déterminer le type exact. Pour reprendre l’exemple de la Collection, cela reviendrait à ne pas pouvoir consulter les données qu’elle contient mais de pouvoir en rajouter…
Si l’intérêt de ces wildcards semble bien limité à première vue, ils prennent tous leurs sens lors de la conception d’APIs car il apporte une valeur ajoutée à la signature des méthodes en apportant un indice sur son utilisation.
Bien entendu il n’y a rien de révolutionnaire là dedans, et il existe sûrement des alternatives en C++ ou .NET pour arriver à des résultats similaires…
Forza Generics ?
Que cela soit bien clair : je n’ai pas écrit ce message pour affirmer la supériorité d’une solution par rapport à une autre, ou d’un langage par rapport à l’autre (merci donc d’éviter de troller ;)).
J’ai simplement voulu présenté un peu plus précisément les avantages de la philosophie des Generics de Java 5.0 qui sont parfois décriés pour leur approche différente d’un même problème. Je ne pense pas vraiment qu’il y ait une solution qui soit meilleure que l’autre, chacune ayant ses avantages, ses défauts et sa propre philosophie…
Et là je m’aperçoit du pavé que je viens de taper sans argumenter d’une seule ligne de code (j’espère que ce sera quand même compréhensible), je remercie ceux qui auront eu la patience (et le courage) de tout lire, et je m’excuse d’avance si j’ai commis des erreurs concernant les mécanismes en place en C++ ou dans .NET…
Enfin pour ceux que le sujet intéresse, un peu de lecture :
- Java 5.0 et les types paramétrés par Romain Guy.
- La section sur Les Generics dans la présentation de Java 5.0 (Tiger) par Lionel Roux.
8 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- Définition exacte de @Override
- Classes, méthodes private
- [ fuite ] memoire
- Possibilité d'accéder au type générique en runtime
- Difference de performances Unix/Windows d'un programme?
- jre 1.5, tomcat 6.0 et multi processeurs
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- Recuperation du nom des parametres
>> Outre les deux liens que tu as donné (au fait celui vers mon texte ne marche pas)
Oups ! Mauvais copier-coller !!! C’est corrigé merci
Il y a aussi la présentation « Effective Java Reloaded » de Joshua Bloch, dispo gratuite sur le site de JavaOne 2006. (Désolé pour les 3 posts mais le site me disait que mon commentaire était invalide quand je les réunissais…)
Outre les deux liens que tu as donné (au fait celui vers mon texte ne marche pas) je conseille vivement deux documents indispensables pour comprendre les generics :
Le tutorial de Sun qui explique pourquoi il faut utiliser les super et extends ainsi que plein de choses :
http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf
L’impressionnante Generics FAQ (433 pages, les réponses à toutes vos questions) :
http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.pdf
Merci pour ce billet adiGuba. J’espère qu’il nous épargnera de futures comparaisons avec les templates C++.
Billet très intéressant. L’avertissement concernant l’utilisation du wildcard est bien venu. J’ai été confronté tout récemment un léger problème de ce côté, et je crois pouvoir affirmer que son utilisation devrait être réservée au paramétrage du type attendu en paramètre. Son utilisation pour spécifier le type de retour abouti souvent à un moment ou un autre à des problèmes de casting.
Concernant la différence entre les Generics et les Templates, j’ajouterai que les Templates ne sont qu’une implémentation du concept plus général de programmation générique. Les Templates et la fameuse STL ont popularisé ce mode de programmation, mais je pense que les Generics de Java respectent mieux la philophie première, ou du moins, comme tu le remarque, donnent corps à un concept jusque présent purement syntaxique.
Si, si, on peut le faire sans problème en C++
D’ailleurs dans le cas des fonctions et méthodes, il y a même des résolutions automatiques du type paramétré qui permet de ne pas s’embêter avec les paramètres de template à indiquer ^^
Par contre, un des soucis en C++, et que si l’on mélange héritage virtuel (multiple) et template, et bien… Il y a un moment où ça passe pas au niveau du polymorphisme (réactions étrange) :-P… Ensuite, je m’y était peut-être mal pris (de toutes façons mon code était une usine à gaz).
Mais bon, c’est aussi chercher un peu le baton pour se faire battre lol ^^
En tous cas, billet très intéressant, Merci
Ce que je veux dire c’est qu’en Java les méthodes d’instances peuvent utiliser un type paramétré différent de celui de la classe, par exemple :
<br />
private E e; <br />
<br />
/* méthode paramétré via la classe */ <br />
public E getValue() { <br />
return this.e; <br />
} <br />
<br />
/* méthode d'instance paramétré différemment de la classe */ <br />
public <T extends Appendable> T appendTo(T to) { <br />
to.append(this.e); <br />
return to; <br />
} <br />
}
Je ne pense pas que ce soit possible en C++ (mais je peux me tromper). Si jamais tu as un exemple qui montre le contraire n’hésite pas
a++
[edit] j’ai réédité car les < > avaient été supprimées…
Juste une remarque : « Si je ne me trompe pas, en C++ seule les fonctions (indépendantes d’une classe) et les définitions de classes peuvent utiliser les Templates » n’est pas vrai. Les méthode peuvent aussi être parametrées.