C'est une réalité de l'industrie de la technologie que les développeurs ne sont jamais plus contents que lorsqu’ils ont de la bière gratuite ou une occasion de se plaindre.
Ainsi, malgré les efforts de Mark Reinhold et l'équipe Java pour impliquer la communauté dans la feuille de route suite à l'acquisition d'Oracle (la décision Plan A / Plan B), de nombreux développeurs Java estiment que Java 7 était « vide de fonctionnalités ».
Dans cet article, je vais essayer de réfuter cette thèse, en explorant les fonctionnalités de Java 7 qui ouvrent la voie aux nouveautés de Java 8.
Opérateur de diamant
Java a été souvent critiqué pour être trop verbeux, le plus généralement dans le domaine de l’affectation. Sous Java 6, nous sommes obligés d'écrire des déclarations d'affectation ainsi :
Map<String, String> m = new HashMap<String, String>();
Cette déclaration contient beaucoup d'informations redondantes : nous devrions pouvoir compter en quelque sorte sur l'intelligence du compilateur pour la résoudre sans avoir besoin que le programmeur soit à ce point explicite.
en fait, les langages comme Scala sont riches en termes d'inférences automatiques à partir d'expressions, ainsi, les instructions d'affectation peuvent être écrites aussi simplement que :
val m = Map("x" -> 24, "y" -> 25, "z" -> 26);
Le mot clé val indique que cette variable ne peut pas être réaffectée (comme le mot-clé final pour les variables Java). Aucune information de type est spécifiée sur la variable, au lieu de cela, le compilateur Scala examine la partie droite de l’affectation et détermine le type adéquat de la variable en consultant la valeur assignée.
Sous Java 7, certaines capacités limitées de détection de type ont été introduites et des instructions d'affectation peuvent désormais être réécrites ainsi :
Map<String, String> m = new HashMap<>();
Les principales différences entre cette forme et la forme Scala reposent sur le fait qu’avec Scala les valeurs ont des types explicites et c'est le type de variables qui en est déduit. Sous Java 7, le type de variables est explicite et le type d'informations sur les valeurs est ce que l'on déduit.
Certains développeurs se sont plaints qu'ils auraient préféré la solution Scala, mais elle se révèle être moins pratique dans le contexte d'une fonctionnalité phare de Java 8 : les expressions lambda.
Sous Java 8, nous pouvons écrire une fonction qui ajoute 2 à un nombre entier comme ceci :
Function<Integer, Integer> fn = x -> x + 2;
L’interface “Function” est nouvelle sous Java 8 - elle existe dans le package java.util.function, avec des formes prédéfinies pour les types primitifs. Cependant, nous avons choisi cette syntaxe car elle est très similaire à Scala et permet au développeur de voir les similitudes facilement.
En spécifiant explicitement le type de fn comme une fonction qui prend un argument entier et retourne un autre entier, le compilateur Java est capable de déduire le type du paramètre x : Entier. C'est le même schéma que nous avons vu sous la syntaxe de diamant Java 7 : on spécifie les types des variables puis on déduit le type de valeurs.
Regardons l'expression lambda Scala correspondante :
val fn = (x : Int) => x + 2;
Ici, nous devons spécifier explicitement le type du paramètre x car nous n'avons pas de type précis pour fn et nous n'avons rien à en déduire. La forme Scala n'est pas extrêmement difficile à lire, mais la forme Java 8 a une certaine propreté de syntaxe qui peut être directement assimilée à la syntaxe diamant de Java 7.
Les méthodes Handles
Les manipulations des méthodes sont à la fois les nouvelles fonctionnalités les plus importantes apportées par Java 7 et les moins susceptibles d'être manipulées quotidiennement par la plupart des développeurs Java.
Une méthode handle est une référence typée pour une méthode à exécuter. Elles peuvent être considérées comme des « pointeurs de fonction typesafe » (pour les développeurs familiers avec C /C++) ou comme « réflexion Core réinventée pour le développeur Java moderne ».
Les méthodes handles jouent un rôle important dans la mise en œuvre des expressions lambda. Les premiers prototypes de Java 8 comprenaient les expressions lambdas converties en classes internes anonymes au moment de la compilation.
Les bêtas plus récentes sont plus sophistiquées. Commençons par rappeler que l'expression lambda (au moins en Java) comprend une signature de fonction (qui dans la méthode handles API sera représentée par un objet MethodType) et un corps, mais pas nécessairement un nom de fonction.
Ceci suggère que l'on pourrait convertir l'expression lambda dans une méthode de synthèse avec une signature correcte et qui contient le corps de la lambda. Par exemple :
Function<Integer, Integer> fn = x -> x + 2;
est transformée par le compilateur Java 8 dans une méthode privée ayant ce bytecode :
private static java.lang.Integer lambda$0(java.lang.Integer);
descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer;
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokevirtual #13 // Method java/lang/Integer.intValue:()I
4: iconst_2
5: iadd
6: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: areturn
Avec une sémantique et une signature correctes (accepte un entier et retourne un autre). Pour utiliser cette expression lambda, nous allons prendre une méthode handle qui se réfère à elle et l'utiliser pour construire un objet de type approprié, comme nous le discuterons dans la suite.
invokedynamic
La dernière caractéristique de Java 7 qui ouvre la porte vers Java 8 est encore plus discrète que les méthodes handles. C'est le nouveau bytecode invokedynamic : le premier bytecode ajouté à la plate-forme depuis Java 1.0. Cette fonction est presque impossible à utiliser pour les développeurs Java 7, car la version 7 javac ne saura, en aucun cas, émettre un ClassFile qui la contient.
Au lieu de cela, le bytecode a été conçu pour être utilisé par les développeurs de langages non-Java, comme JRuby, qui exigent beaucoup plus de souplesse que Java. Pour voir comment fonctionne invokedynamic, abordons la manière dont l’appel à la méthode Java sera compilé en bytecode.
Un appel à une méthode Java standard sera transformé en parties de bytecode JVM , qui est souvent désigné comme une invocation locale. Celui-ci comprend un opcode de délégation (comme invokevirtual pour les appels de méthode d'instance ordinaire) et une constante (un offset dans le pool de constantes de la classe) qui indique la méthode à appeler.
Les différentes répartitions opcodes ont diverses règles régissant l’exécution de la méthode lookup, mais jusqu'à la version Java 7, la constante a toujours été une indication directe de la méthode à appeler.
Invokedynamic est différente. Au lieu de fournir une constante qui indique directement la méthode qui doit être appelée, invokedynamic fournit à la place un mécanisme d'indirection qui permet au code utilisateur de décider quelle méthode à appeler lors de l'exécution.
Quand un site invokedynamic est rencontré au départ, il n'a pas encore une cible connue. Au lieu de cela, une méthode handle (appelée méthode bootstrap) est invoquée. Cette méthode bootstrap retourne un objet CallSite qui contient une autre méthode handle et qui est la cible réelle de l'appel invokedynamic.
1) Appel à invokedynamic dans le flux d'exécution (initialement non lié) 2) Invoquer la méthode bootstrap et retourner un objet CallSite 3) l’objet CallSite contient une méthode handle (la cible) 4) Invoquer la méthode handle cible
La procédure d'amorçage est la manière avec laquelle le code utilisateur choisit la méthode qui devrait être appelée. Pour les expressions lambda, la plate-forme utilise une méthode bootstrap fournie dans la bibliothèque appelée lambda meta-factory.
Celle-ci a des arguments statiques qui contiennent une méthode handle vers la méthode de synthèse (voir section précédente) et la signature correcte pour la lambda.
La meta-factory renvoie un CallSite qui contient une méthode handle, qui à son tour, retourne une instance de type correct auquel l'expression lambda l’a converti. Ainsi, une telle déclaration :
Function<Integer, Integer> fn = x -> x + 2;
est convertie en un appel invokedynamic comme ceci :
Code:
stack=4, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
5: astore_1
La méthode de bootstrap invokedynamic est la méthode statique LambdaMetafactory.metafactory() qui retourne un objet CallSite qui est lié à une méthode cible handle qui renverra un objet implémentant l'interface de fonction.
Lorsque l'instruction invokedynamic est terminée, un objet qui implémente Function et qui admet l'expression lambda comme contenu de sa méthode apply(), est placé au sommet de la pile et le reste du code continue à se dérouler normalement.
Conclusion
Avoir les expressions lambda dans la plate-forme Java a toujours été une tâche difficile, mais en veillant à ce que les bonnes bases soient en place, Java 7 a considérablement facilité cet effort. Le Plan B a non seulement fourni aux développeurs la publication anticipée de Java 7, mais il a également permis aux technologies clé d’être pleinement testées sur terrain avant leur utilisation sous Java 8 et en particulier dans les expressions lambda.
À propos de l'auteur
Ben Evans est le PDG de jClarity, une start-up qui fournit des outils de performance pour aider les équipes de développement et d’opération. Il est un des organisateurs LJC (London JUG) et membre du JCP Executive Committee qui contribue à définir des normes pour l'écosystème Java. Il est champion Java ; JavaOne Rockstar, co-auteur de « The Well-Grounded Java Developer » et animateur régulier de conférences publiques sur la plate-forme Java, la performance, la concurrence et les sujets connexes.