1. Bonjour José, peux-tu te présenter ?
Bonjour, José Paumard. Je suis universitaire, je travaille à l'université Paris 13 où j'enseigne les technologies Java et la plateforme Java EE depuis 1998 (15 ans). Je travaille également sur des projets pour des entreprises, en expertise ou en formation. Et enfin, en tant que membre de l'association Paris JUG, je co-organise avec Antonio Goncalves, Zouheir Cadi et Nicolas Martignole, Devoxx France depuis 2 ans.
En fait, les Lambda n'est pas un concept récent, mais qui vient de la notion de lambda-calcul qui est apparu dans l'informatique fondamentale des années 30, donc même bien avant les premières conceptions de langage structuré. C'est vraiment une nouveauté à l'intérieur, à la fois de la JVM, du langage, des APIs. C'est quelque chose qui a un impact extrêmement large sur tout ça, et qui va également avoir un impact sur la façon dont la JVM fonctionne, la façon dont on écrit les applications et aussi la façon dont on exploite les performances des CPU, des micro-processeurs.
En informatique, il y a des modes qui viennent et qui passent. La programmation fonctionnelle n'est pas un idée nouvelle puisque les premiers concepts de programmation fonctionnelle datent du Lisp, qui fait partie des premiers langages qui ont été introduits en informatique dans les années 50. Si le Lisp ou si les langages tels que le Lisp avaient été une panacée, je pense qu'il y aurait plus que ça aujourd'hui, or ce n'est pas le cas. Il y a toujours des langages construits sur le concept du Lisp, en Clojure sur la JVM, qui est bien connu. Clojure par exemple n'envahit pas l'espace, c'est ce qu'on constate, c'est un bon langage, il n'y a aucun doute là-dessus, mais ça n'envahit pas l'espace. La programmation fonctionnelle apporte sans aucun doute un certain nombre de réponses, notamment dans le cas de la programmation multi-thread ou du calcul parallèle. Est-ce que c'est une panacée? Est-ce quelque chose qui va vraiment complètement prendre la place de langages tels que Java, ou tels que .Net ? Franchement je ne crois pas, en tout cas pas dans les années qui viennent.
Ce n'est pas seulement lié à des modifications profondes de la JVM. Les Lambda, c'est une modification de la syntaxe du langage, donc il y a déjà des modifications au niveau du compilateur, javac. C'est lié à des modifications extrêmement importantes de l'API Java, c'est-à-dire tout ce qu'il y a dans rt.jar grosso modo, de toutes les APIs que l'on utilise au quotidien. Et puis c'est également une modification de la JVM en interne, qui ne va pas fonctionner exactement de la même façon dû à la présence des Lambda. Elle va fonctionner d'une nouvelle manière pour les prendre en compte. Ca va aussi "devoir être" - ce n'est pas une obligation mais ce serait une erreur de ne pas le prendre en compte - une modification des habitudes de programmation. Essentiellement, avec les Lambda, on a un nouveau concept qui arrive et qui casse le paradigme fondateur du langage Java. Dans Java, tout est objet, tout doit être déclaré dans une classe. Java a été créé au début des années 90 et est sorti en 1995; le C++ avait déjà plusieurs années derrière lui, et en C++, pas obligation de tout mettre dans des classes. En Java, pour des raisons de simplification, les concepteurs ont décidé de mettre tout dans des objets et dans des classes. Les Lambda viennent casser ceci en permettant d'écrire des morceaux de code qui vont pouvoir être exécutés et qui ne devront pas vivre dans des classes ou dans des objets. C'est une véritable modification, même si dans un premier temps, la JVM ne va peut être pas fonctionner comme ça (elle va continuer à mettre des Lambda dans des objets), à terme elle ne sera pas obligé de le faire, ce qui va lui permettre de faire des optimisations qui aujourd'hui ne sont pas possibles, notamment dans le domaine du calcul multi-thread et du calcul parallèle. Au niveau de l'impact sur les APIs, il est énorme. A Devoxx Belgique, un des représentants d'Oracle a expliqué que l'impact de l'arrivée des Lambda sur le JDK, en terme de nombre de lignes de code à modifier, est supérieur à l'impact de l'arrivée des types paramétrés, donc des génériques en Java 5. Ce fût énorme, une réécriture quasi complète du JDK. Et bien l'arrivée des Lambda dans le JDK 8 va être encore plus importante comme impact. Cela touche tous les secteurs, tous les différents grands thèmes des APIs du JDK.
5. Et si on prend en particulier le framework des collections Java ?
Pour l'arrivée des Lambda, le framework Collection va être largement réécrit. Il a déjà été réécrit une première fois pour Java 5 afin de permettre d'intégrer les types paramétrés. C'est une réécriture qui va être plus importante que Java 5, pourquoi? Car la réécriture de Java 5 ne modifiait pas la façon dont on utilisait le framework Collection. La principale modification c'est que quand on faisait list.get par exemple, cela retournait une chaîne de caractère, on avait plus besoin de caster l'Object qui était retourné en Java 4 en chaîne de caractères. Mais on continuait à utiliser le framework de la même façon. Là le framework Collection est profondémment modifié, non pas parce que l'on rajoute beaucoup de méthodes dans les interfaces, mais parce qu'on lui rajoute à côté l'API Stream qui va permettre de faire ce qu'on faisait avant sur les collections avec les itérateurs, mais de façon externe et de façon beaucoup plus performante. Le premier impact est là, on ne va plus programmer les collections avec l'API Collection de façon classique comme jusqu'en Java 7, mais on va plutôt passer nos collections dans des streams et programmer les traitements de collection sur ces streams. L'apport de ces stream est d'optimiser les choses, notamment en faisant du calcul parallèle sur tous les coeurs du processeur. Donc cette API Stream, c'est la véritable révolution, avec des nouveaux patterns d'utilisation, qui vont vraiment changer la façon dont on écrit les traitements sur les collections, et notamment les collections de grande taille.
6. Et par rapport donc au multi-threading, que va apporter l'API Stream ?
Alors, l'API Stream est aussi faite pour permettre de paralléliser les traitements. Alors il y a plusieurs aspects, mais je vais commencer par définir le traitement multi-threading et le traitement parallèle, car les deux choses sont fondamentalement différentes. Le traitement multi-thread, c'est d'avoir tout un tas de traitements à faire, par exemple dans le cadre d'une application classique d'informatique de gestion, j'ai tout un tas de coeurs disponibles sur mon processeur, plusieurs dizaines, plusieurs centaines, dans les années qui viennent j'en aurais peut-être plusieurs milliers, et donc je veux distribuer ces traitements chacun sur un thread, et que chacun de ces threads s'exécute sur un coeur de mon processeur. Ça c'est du multi-threading. On a déjà des APIs permettant de traiter ce problème depuis Java 5, depuis l'API java.util.concurrent. La problématique c'est celle de la synchronisation, de visibilité, de transfert de données d'un thread à l'autre, donc d'un cache d'un coeur de processeur vers un autre, avec les problèmes classiques et bien connues aujourd'hui que cette approche induit. Le traitement parallèle, c'est quelque chose de très différent. J'ai un traitement à faire, un traitement extrêmement lourd, calculatoire. Comme j'ai une centaine de coeurs, je veux distribuer ce traitement sur tous mes coeurs. On peut penser au mapping d'une collection, par exemple de plusieurs dizaines de millions d'éléments. J'ai 8 coeurs, je suis sur un Core i7 classique, j'aimerais bien distribuer mes dix millions d'éléments sur mes 8 coeurs et faire en sorte que le mapping se déroule en parallèle sur ces 8 coeurs. Donc dans cette approche, j'ai un traitement et je veux le découper en petits bouts, et je veux l'exécuter sur tous mes coeurs en parallèle. On a des outils pour faire ça depuis Java 7, avec une rétro-compatibilité Java 6, dans le framework Fork/Join qui est intégré au JDK. Le framework Fork/Join est un framework bas niveau, un peu complexe et délicat à utiliser, et qui surtout impose au développeur d'écrire lui-même la parallélisation de son traitement. Et ça c'est un problème car la parallélisation d'un traitement ce n'est pas quelque chose de trivial. Il y a des algorithmes qui se parallélisent mal, comme par exemple les algorithmes de tri. On peut écrire du code parallèle mais se rendre compte qu'il peut être moins performant que celui qui ne l'est pas, même si cela peut paraître paradoxal c'est comme ça. Et puis il y a les algorithmes qui ne sont pas parallélisables du tout. Et là c'est encore plus délicat, encore plus subtil. Parce qui si je les parallélise, j'aurais un résultat, sauf que ce résultat sera complètement faux. L'API Stream permet, juste par l'appel d'une méthode parallel, de lancer des traitements sur des collections de grande taille et de paralléliser ces traitements sur l'ensemble des coeurs de mon processeur. Et c'est juste un appel de méthode qui permet de faire ça. C'est fondamental car cela veut dire que la parallélisation du traitement que je veux faire est entièrement gérée par l'API, je donne juste ma fonction lambda et ma collection, et la distribution sur les coeurs est entièrement gérée par l'API, en tant que développeur je n'ai rien à faire. Ça libère donc les développeurs de l'écriture de ce traitement parallèle, qui est délicat. Le framework Fork/Join est en-dessous, si je prends la ConcurrentHashMap qui est dans Java 8, qui est une réécriture complète de la version Java 7, elle embarque son propre framework Fork/Join pour gérer et lancer les traitements que je lui donne directement sur tous les coeurs. C'est un apport extrêmement important. C'est stratégiquement et commercialement important que Java propose un mécanisme de la sorte, car l'exploitation de la puissance des processeurs depuis 5 ou 6 ans, c'est quelque chose qui est absolument central en informatique. Le fait de donner la possibilité de le faire de façon extrêmement simple ces traitements dans un langage mainstream comme Java, c'est vraiment une avancée tout à fait majeure. Donc Java se positionne fortement sur ce segment-là, comme un des langages tout à fait à la pointe et à jour.
Depuis vingt ans que Java existe, on a pas mal de livres qui ont été publiés sur les patterns. Le livre du Gang of Four, la première édition, a vingt ans, et une nouvelle édition est prévue pour les mois qui viennent. Donc ce sont des patterns qui sont toujours vivants. Même si certains sont encore très utilisés comme le Decorator, comme le Builder, il y en a d'autres qui ont un petit peu plus de plomb dans l'aile, comme le Singleton, maintenant que l'on a des JVMs distribuées sur des clusters, la notion de Singleton commence à un peu moins bien fonctionner, sans compter aussi les problèmes que peuvent poser les créations de singletons lorsque l'on est en multi-thread. Les patterns du Gang of Four ont été écrits avant la sortie du langage Java, et se voulaient agnostiques à l'origine donc ce sont vraiment des patterns de programmation pure, et ils sont un peu anciens. Par exemple, ils ne parlent pas du tout de programmation multi-threadée, alors que c'est au centre de la majorité des applications. En programmation fonctionnelle, l'approche consiste à dire, on utilise des objects immutables, on réalise des traitements dessus, et lorsque l'on veut changer l'état de ces objects, ce qui n'est pas possible, on crée des copies. Cela a été appliqué dans la plupart des langages construits sur les principes de Lisp, par exemple Clojure fonctionne comme cela. Cela a été appliqué pour les implémentations de l'API Collection, que ce soit les listes chaînées ou les tables de hachage. Le premier intérêt, c'est qu'avec des objects immutables on peut les distribuer entre les threads sans synchronisation. Cela dit, créer des objets immutables cela a aussi un overhead, donc il ne faut pas rêver, ce n'est pas non plus une panacée. C'est une solution parmi d'autres, qui peut être utilisée. Cela dit, des nouveaux patterns avec Java 8, c'est sûr qu'il va y en avoir, il n'y a pas de doute là-dessus. Il y en a eu avec Java 5, le livre de Brian Goetz, Java Concurrency in Practice, apporte plein de nouveaux patterns en matière de programmation multi-thread notamment. Avec Java 8 et cette API Stream, on va avoir toute un flopée de nouveaux patterns qui vont arriver, et qui vont révolutionner la façon dont on travaille en tant que développeurs, mais également les performances que l'on va pouvoir atteindre quand on va exécuter ce type de traitements.
Oui, au niveau mémoire et au niveau performance aussi. Il y a des gens qui colportent encore l'idée que Java est lent, notamment par rapport au C et au C++. Ce n'est plus vrai depuis 7 ou 8 ans. Pour tout un tas de raisons. Il y a même des contextes d'exécution dans lequel le code Java compilé par le compilateur Just-in-Time (JIT) est plus performant que le code que l'on pourrait écrire à la main. Je ne sais pas si on peut parler de légende urbaine, ce sont de vieilles idées qui traînent encore aujourd'hui, y compris dans les articles de blogs peu informés. Ce qui est certain, c'est que cette histoire de Lambdas, pour revenir à l'idée de base, c'est qu'une lambda n'est pas forcément un objet. Donc si ce n'est pas nécessairement un objet, c'est que ce n'est pas nécessairement une classe. Grosso modo, en première approche, les lambdas vont permettre les instances de classes anonymes que l'on utilise partout, de façon différente. Sauf qu'une instance de classe anonyme, il y a "instance" et "classe" dedans; c'est quelque chose qui se charge dans la JVM, qui doit être gérée, à la fois en termes de sécurité, d'espace mémoire, qui doit être stockée, entretenue, etc. Après je crée une instance de cette classe anonyme, cette instance aussi il faut la gérer, elle va vivre en mémoire, elle va probablement avoir une durée de vie assez courte, donc derrière il va falloir la garbager donc cela va également être une surcharge pour le Garbage Collector (GC, ramasse-miettes). Dès l'instant où le code qui est dans mon instance de classe anonyme, passe dans quelque chose qui n'est plus une instance de classe, je n'ai plus besoin de charger la classe, plus besoin d'instancier l'objet, et donc plus besoin de le garbager. Donc je gagne sur les deux tableaux; à la fois sur celui de la mémoire, car il y a une partie de la mémoire que je consacrais au stockage de la classe et au stockage de l'objet; et je gagne également en performance, je n'ai plus besoin de tester la validité de la classe, la sécurité, etc, et mon GC n'a plus besoin de garbager cet objet. Donc oui, le premier impact, de niveau zéro je dirais, va me permettre de gagner énormément en mémoire. Maintenant si on regarde les patterns de l'API Stream dont je parlais à l'instant, même un bête pattern map-filter-reduce, si je l'applique à une collection, je me rends compte que je crée des intermédiaires de calcul qui sont des duplications de ma collection originelle. Je fais un mapping, je crée un intermédiaire de calcul qui est le résultat de ma première. Et puis après j'applique un filtrage, donc je crée une seconde collection qui est le résultat du filtrage de la précédente. Et ensuite je fais une réduction, donc j'itère sur la précédente collection filtrée pour calculer ma réduction. Donc j'ai deux intermédiaires de calcul, et je pourrais en avoir beaucoup plus de deux si je prends plusieurs modules dans mon application qui communiquement mal. Un premier module prend tous les entiers dans ma collection supérieurs à 20, ensuite le deuxième module reprend cette collection et filtre tous les entiers supérieurs à 50, et c'est stupide car j'ai déjà filtré tous les entiers supérieurs à 20. Le fait de créer des intermédiaires de calcul et de les multiplier sans avoir de visibilité sur le traitement complet map-filter-reduce, m'empêche de faire tout un tas d'optimisations. Le fait d'utiliser l'API Stream, je vais prendre ma collection, créer un stream dessus, et puis je vais définir un certain nombre d'opérations, et ces opérations je ne vais les exécuter qu'à la fin de la spécification du pipeline de traitement que je vais avoir à faire dessus. Et ça c'est génial car premièrement je ne fais aucun calcul intermédiaire donc je ne génère pas d'états intermédiaires, donc potentiellement je peux gagner un facteur 2, 3, potentiellement autant que d'étapes dans mon pipeline de calcul. Et ensuite je peux relire mon filtrage et me rendre compte que je fais un filtrage sur les entiers supérieurs à 20, puis ensuite supérieurs à 50, et que donc le premier filtrage n'est pas utile, donc je peux également simplifier le calcul. Donc ces patterns là vont me permettre de gagner sur tous les points, à la fois au niveau du langage mais aussi au niveau applicatif, par la suppression de la génération de ces états intermédiaires d'une part, d'autre part par la possibilité que l'API va avoir de supprimer les étapes non nécessaires, de façon à limiter les traitements.