09 octobre 2012

Good Bad and Ugly generics

Ce talk était un véritable show, entièrement réalisé en live coding. Speaker boosté aux stéroïdes, humour et contenu, une super session.
Dans le texte qui suit, je note les génériques avec des accolades carrées "[ ]" car blogger est une grosse bouse qui ne gère pas l'échappement des balises html. Je n'ai pas non plus réussi à éviter les &amps ... 



Les generics ont été introduites dans Java pour résoudre un problème qui n’existe QUE parce que le langage est statiquement typé. Cette version « simplifiée » des templates C++ a fait couler beaucoup d’encre, entre autre pour débattre de sa syntaxe particulièrement … pourrie.

Je vous passe la partie « good » de la présentation, les génériques permettant en substance d’écrire moins de code et d’éviter de se tromper en mettant des oranges dans un panier de bananes.

Si on décompile un bout de code qui manipule une List[foo] on constate qu’il ne subsiste rien du paramètre de type dans le bytecode. Cet effacement, nécessaire pour assurer la compatibilité, fait des génériques un simple « sucre syntaxique » pour éviter au développeur de caster ses variables, opération qui sera bel et bien présente au final dans le bytecode (checkcast). Les choses se compliquent lorsque vous créez une méthode foo(List[fruit]) qui veut naivement ajouter une Orange dans le panier. Le compilo vous insulte. C’est là que l’on mesure l’intelligibilité de ces génériques, sur lequel Dr Odersky (qui créa Scala par la suite) a bossé, faut-il y voir une relation de cause à effet (#troll et mauvaise foi inside). 

Il faut donc écrire foo(List[? extends Fruit]), pour lequel le mot clé extends n’a RIEN à voir avec le sens classique du terme, comme si un mot clé plus spécifique n’avait pas pu être introduit ici, genre le compilo est très con il ne sait pas identifier un mot comme réservé lorsqu’il est dans une expression générique et le laisser libre dans les autres cas … j’ai toujours été ébahi que le mot clé « class » ne puissent être utilisé comme nom de variable, alors qu’il n’est utilisable dans le langage QUE pour des éléments très spécifiques et facilement identifiables. Ca montre que le compilo est basé sur une analyse lexicale puis sémantique à la grand papa, dommage.


On passe ensuite à la paramétrisation (ça se dit ça ?) des classes, et un petit rappel du principe de substitution de Liskow, qui nous amène à la conclusion qu’on ne devrait quasiment pas utiliser l’héritage et lui préférer la délégation, ce qu’on ne fait évidement jamais en java tellement il est simple d’écrire Foo extends Bar - à moins d'utiliser lombok et @Delegate, rendez-vous au prochain JUG pour en savoir plus. Après cette parenthèse, la leçon à retenir est que List[orange] n’étend pas List[fruit], certes à juste titre, mais qui crée en général des nœuds au cerveau pour les développeurs novices.

Pour copier les oranges d’un panier d’orange dans un panier de fruit il faudra donc écrire : copy(List[T] source, List[? super T] target)
une nouvelle fois en utilisant un mot clé existant dans un contexte ou son sens est complètement détourné et peu intelligible. Nos génériques, avec une syntaxe de m…, sont nées pour mourir à la compilation, ce qui porte le doux nom de Type Erasure. Confronté au même problème, C# a choisi de casser la compatibilité ascendante pour promouvoir la réification de type (essayez de placer ça dans la conversation, ça fait mec intelligent).

L’effacement de type n’est pas systématique tout de même, ainsi un [T extends Comparable] sera traduit dans le bytecode par un type Comparable. Par contre, un [T extends Comparable & Serializable] - et oui, on peut aussi faire ça, le saviez vous ? pas moi - ne verra plus aucune trace du Serializable dans le bytecode. Le « & » dans une expression générique n’est donc pas commutatif ! fun non (ou très con, selon les goûts) ?

On passe rapidement sur l’absence de support pour les types primitifs, qui nous collent ainsi du boxing/unboxing caché, source de NullPointerException, sans doute mon bug préféré. Un petit mot sur l’impossibilité de construire une instance du type paramétré (sauf à bricoler avec de la réflexion), et on enfonce le clou avec cet exemple : partant d’une interface :
EventListener[t] { void onEvent(T evt) }
vous ne pouvez pas créer une classe Foo implements EventListener[String], EventListener[number], car le code Type-Erasé subira une collision de méthode !

Pour conclure, on prend un exemple ou une classe paramétrée Foo[T] tente de maintenir un compteur statique de son nombre d’instance. Le compteur est alors global, et non par type considéré, et comble du bonheur on ne peut même pas écrire Foo[String].getCount() sans se faire insulter par le compilateur.



Bref, si certains arguments touchent à des utilisations peu communes des génériques, le talk aura proposé un tour assez complet de leurs impacts sur Java, aussi bien au runtime que dans le code source, et démontré la nécessité de toujours définir l’usage attendu des arguments de méthode lorsqu’il sont paramétrés, selon qu’on veut en extraire des données (extends) ou en ajouter (super).

2 commentaires:

Batmat a dit…

Je suis en train de parcourir les articles sur JavaOne. Bravo pour le boulot et très intéressant, Nicolas.

Quelques remarques pour contribuer un peu :-) :
* "Je vous passer" => passe
* "mauvaise foie" => foi. D'ailleurs, à ce propos, c'est amusant que tu parles de ça puisqu'en ce moment on en est à la semaine 4 du cours de coursera, et qu'on y explique justement la covariance (Si A hérite de B, alors on peut dire que List[A] hérite de List[B] si List est covariant).
* "classique du termes" => terme
* très spécifique => spécifiques ?
* "ne vera plus" => verra
* gouts => goûts

Baptiste

nicolas deloof a dit…

@Batmat corrigé, merci