Java 8 et les Lambda
La prochaine version de Java, prévue pour 2013, apportera le lot de nouveautés le plus important depuis Java 5 : expressions lambda, “public defender methods“, références de fonctions…
Aujourd’hui, je vous propose de découvrir la nature, la syntaxe et les cas d’utilisation des expressions lambda (une forme de “closure”), ainsi que leur impact sur notre façon de coder.
Lambda ?
Le problème
En Java, il n’existe que deux types de références : des références vers des valeurs primitives, et des références vers des instances d’objets.
int i = 42; // Référence vers une valeur primitive String s = "42"; // Référence vers une instance
Dans d’autres langages (Groovy, Scala, Haskell…), il est également possible d’établir des références vers des closures, c’est-à-dire des blocs de code anonymes.
Une référence de ce type peut alors, comme toutes les autres, être utilisée en tant que champ d’une classe ou en paramètre d’une méthode.
Ce dernier usage est très répandu dans les langages fonctionnels (ou “orientés fonctionnel”). En particulier,la possibilité de passer une fonction en argument d’une autre permet leur composition, favorise leur découpage atomique, simplifie leur test, et améliore leur réutilisabilité.
En Java, qui ne dispose pas de cette facilité, la technique qui s’en rapproche le plus consiste à définir une interface décrivant la fonctionnalité souhaitée, puis à instancier une classe (souvent anonyme) implémentant la fonctionnalité. L’instance obtenue peut alors être affectée à une référence et/ou être passée en paramètre d’une méthode.
Mais cette façon de faire est très verbeuse, comme nous allons nous en rendre compte à l’aide de l’exemple suivant.
Comparaison Groovy / Java
Nous souhaitons afficher tous les éléments d’une liste qui satisfont un certain critère arbitraire. La méthode d’affichage étant générique, il est nécessaire de lui passer en paramètre l’algorithme de filtrage.
Comparons les implémentations en Groovy, qui dispose des closures, et en Java, qui n’en dispose pas (encore).
Groovy :
def names = ["un", "deux", "trois", "quatre"] // names : la collection // filter : l'algorithme de filtrage def printNames(names, filter) { println names.findAll(filter) } // Critère de filtrage, sous forme de closure // On ne conserve que les noms courts (5 caractères max) def isShortName = {name -> name.size() <= 5} printNames (names, isShortName)
La syntaxe est claire et lisible. Notez la façon dont un bloc de code anonyme (closure) est directement affecté à la référence isShortName
, puis passé en paramètre de la méthode printNames()
.
Maintenant, en Java :
public class PrintNames { // Encapsule la définition de la méthode implémentant le critère de sélection private interface Predicate<T> { public boolean keep(T element); } public static <T> void printNames(List<T> elements, Predicate<T> filter) { for (T elt : elements) { if (filter.keep(elt)) { System.out.println(elt); } } } public static void main(String[] args) { List<String> names = Arrays.asList("un", "deux", "trois", "quatre"); // Critère de sélection, sous forme de classe anonyme Predicate<String> isShortName = new Predicate<String>() { @Override public boolean keep(String element) { // La seule ligne réellement utile ! return element.length() <= 5; } }; printNames(names, isShortName); } }
Je pense que la différence saute aux yeux. Le code Java est nettement plus verbeux, et noie la fonctionnalité métier au sein d’une masse importante de code purement technique.
Voyons quelle solution Java 8 propose.
Les Lambda en Java 8
Domaine d’application
Sous la pression des langages “alternatifs” et de la communauté Java, Oracle s’est enfin décidé à intégrer les closures dans le langage.
Enfin… pas tout à fait.
Pour des raisons de rétrocompatibilité avec le type-system existant, Java 8 limitera sévèrement leur domaine d’application. Les closures ne serviront en réalité qu’à simplifier l’implémentation et l’utilisation des “Interfaces SAM” (“Single Abstract Method”) ou “Interfaces fonctionnelles”, c’est-à-dire les interfaces ne définissant qu’une seule méthode[1].
Certes, ces interfaces sont nombreuses en Java : Runnable
, Callable
, Comparator
, ActionListener
… Et de nombreux frameworks orientés événements (en particulier les frameworks graphiques comme Swing ou GWT) les utilisent pour déclarer des callbacks.
Mais tout de même, on est loin de la souplesse et de la puissance des closures présentes dans les autres langages.
Sous le capot
A la compilation, les closures sont tout simplement compilées sous la forme de simples classes anonymes.
Par exemple, le code suivant…
public class CompilationTest { interface Doubler { int timesTwo(int x); } public static void main(String[] args) { Doubler d = (n) -> { return n * 2;}; } }
… est compilé sous la forme de 3 classes :
CompilationTest # La classe de test CompilationTest$Doubler # L'interface Doubler CompilationTest$1 # La closure implémentant Doubler
Si nous décompilons la classe CompilationTest$1
, nous obtenons :
Compiled from "CompilationTest.java" class CompilationTest$1 implements CompilationTest$Doubler { CompilationTest$1(); public int timesTwo(int); }
En descendant au niveau du bytecode, nous retrouvons bien l’opération de multiplication par deux :
public int timesTwo(int); flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: iload_1 1: iconst_2 2: imul 3: ireturn
Etudions maintenant la syntaxe.
Syntaxe
Une closure peut se concevoir comme une méthode anonyme. A ce titre, elle peut accepter des paramètres et retourner un résultat.
Après moult tergiversations et guerres des tranchées sur la mailing-list dédiée, la syntaxe retenue est inspirée de celle de C#[2]. Elle peut prendre deux formes :
(paramètres) -> expression_simple
(paramètres) -> { bloc_d'instructions }
Exemples :
(int x) -> x * 2 #1 (int x) -> { return x * 2; } #2 (String s) -> { System.out.println(s); } #3 () -> 42 #4
Explications :
La première expression prend un paramètre x
de type int
, et renvoie le double de sa valeur. Notez l’absence du mot-clé return
: la valeur de l’expression est automatiquement renvoyée.
La seconde est une variante de la première, qui utilise un bloc d’instructions. Cette fois, le mot-clé return
est nécessaire.
La troisième expression accepte un paramètre de type String
mais ne renvoie rien.
Enfin, la quatrième expression ne prend aucun paramètre, et renvoie la constante 42
.
Syntaxe simplifiée
Cette syntaxe peut être encore simplifiée dans certains cas :
- Si les types des paramètres peuvent être inférés, il n’est pas nécessaire de les préciser.
- Les parenthèses sont optionnelles si la closure n’attend qu’un seul paramètre (elles sont par contre obligatoires pour zéro paramètres).
Les trois expressions suivantes sont donc équivalentes :
(String s) -> s.length() (s) -> s.length() s -> s.length()
Capture de variables
Actuellement, les classes anonymes ne peuvent accéder aux variables de leur environnement d’exécution que si celles-ci sont déclarées final
.
Il est toutefois prévu de relâcher quelque peu cette contrainte en Java 8, et d’autoriser également la capture des variables “effectivement finales”, c’est-à-dire qui ne sont pas explicitement déclarées final
, mais dont la valeur n’est jamais modifiée après leur première initialisation. Les closures bénéficieront également de cette facilité.
Exemple : récupération des nombres inférieurs à 3
int max = 3; // Non final, mais "effectivement final" List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5); Iterable<Integer> smallNums = nums.filter(n -> n < max);
Les closures en action
Les closures servant avant tout à faciliter l’implémentation des “interfaces SAM”, on peut les affecter à des références de type interface.
Runnable
Prenons l’exemple d’un Runnable
, qui définit une unique méthode run()
, n’acceptant aucun argument et ne renvoyant aucun résultat.
Traditionnellement, nous l’implémentons comme ceci :
Runnable job = new Runnable() { @Override public void run() { System.out.println("Hello world"); } };
Avec les closures, nous pourrons le définir comme cela :
Runnable job = () -> { System.out.println("Hello world"); };
Notez que le code est considérablement simplifié, et recentré sur l’algorithme métier. La lisibilité est également meilleure.
ActionListener
Prenons un second exemple, un peu plus complexe : l’interface ActionListener
, qui permet de réagir au clic sur un bouton Swing. Elle définit une méthode actionPerformed(ActionEvent event)
– notez la présence du paramètre de type ActionEvent
.
Au lieu de l’affecter à une référence, nous allons cette fois la passer directement en paramètre de la méthode JButton.addActionListener()
.
Sans les closures :
JButton greeterButton = new JButton("Click me !"); greeterButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { JOptionPane.showMessageDialog(null, "Hello !"); } });
Avec les closures :
JButton greeterButton = new JButton("Click me !"); greeterButton.addActionListener( event -> { JOptionPane.showMessageDialog(null, "Hello !"); });
Comparator
Un dernier exemple pour la route, avec l’interface Comparator<T>
, qui expose la méthode int compare(T x, T y)
, qui accepte deux paramètres :
List<String> strings = Arrays.asList("hello", "world", "!"); Collections.sort(strings, (s1, s2)-> s1.compareTo(s2)); System.out.println(strings);
Principaux impacts
On l’a vu, le domaine d’application des expressions lambda en Java sera relativement limité.
Essayons d’en faire le tour…
Le JDK
Evidemment, le premier bénéficiaire sera le JDK lui-même. Les classes pré-existantes ne seront sans doute pas adaptées, mais l’on peut d’ores et déjà voir les lambda en action dans certaines nouvelles classes ou méthodes.
Dans la dernière “developer preview”, j’ai dénombré les usages suivants :
java.lang.MapStream
(nouvelle classe)
java.util.Arrays
(nouvelle méthode iterable()
, et nouvelle classe interne ArraySplittable
)
java.util.ParallelIterables
(nouvelle classe, fonctionne avec Fork/Join pour réaliser des opérations en parallèle sur les éléments d’une collection)
Ainsi que dans le tout nouveau package java.util.functions
, dédié à la programmation fonctionnelle :
java.util.functions.Blocks
(nouvelle classe utilitaire pour manipuler des Block
s)
java.util.functions.Mappers
(nouvelle classe utilitaire pour manipuler des Mapper
s)
java.util.functions.Predicates
(nouvelle classe utilitaire pour manipuler des Predicate
s)
Les frameworks orientés événements
Les interfaces à méthode unique sont très utilisées, dans les frameworks orientés événements (en particulier des frameworks graphiques), pour implémenter des callbacks. On pourrait donc s’attendre à ce que les lambda y trouvent un terrain d’application naturel.
AWT /Swing
Le package java.awt.event
regroupe toutes les interfaces permettant de répondre aux événements graphiques.
Sur les 18 interfaces, 8 seulement ne définissent qu’une seule méthode, et sont donc éligibles au titre d'”Interface SAM” (dont ActionListener
déjà vu plus haut). Les autres interfaces définissent plusieurs méthodes, et ne seront donc pas implémentables sous forme d’expression lambda.
Côté Swing, l’étude du package javax.swing.event
aboutit au même constat : seulement 9 interfaces sur 23 sont éligibles.
Un intérêt mitigé côté AWT/Swing, donc.
Un exemple d’utilisation :
JButton greeterButton = new JButton("Click me !"); greeterButton.addActionListener( (event) -> { JOptionPane.showMessageDialog(null, "Hello !"); });
Wicket
Wicket est un framework web orienté composants, ressemblant à Swing par bien des aspects.
Malheureusement, son modèle de callback est complètement différent : ceux-ci sont définis comme des méthodes internes aux composants graphiques, que le développeur doit surcharger.
Par exemple, pour réagir au clic sur un lien :
Link link = new Link("linkId") { public void onClick() { // ... } );
A moins d’un changement majeur dans l’architecture du framework, il sera impossible d’utiliser les expressions lambda pour simplifier le déveoppement d’applications avec Wicket.
GWT
Le cas GWT est intéressant.
Contrairement à Wicket, ce framework de RIA utilise bien des interfaces pour gérer les callbacks :
Button button = new Button("Click me !"); button.addClickListener(new ClickListener() { public void onClick(Widget sender) { Window.alert("Hello, world"); } });
GWT pourrait donc être un excellent candidat pour les expressions lambda :
Button button = new Button("Click me !"); button.addClickListener( sender -> { Window.alert("Hello, again"); });
Mais le code GWT n’est pas exécuté tel quel : il est d’abord traduit en Javascript. La capacité d’utiliser des expressions lambda dépendra donc directement de leur prise en compte dans le traducteur Java vers Javascript…
Vu le travail que cela représente et le gain somme toute modéré qu’on peut en attendre, je doute que cette fonctionnalité soit jamais implémentée. Wait & see…
Dans nos projets
Pour finir, il sera naturellement possible d’intégrer le support des expressions lambda dans nos projets, partout où nous aurons défini des interfaces compatibles SAM.
Certains use-cases s’y prêteront plus naturellement, comme les callbacks ou handlers. Certains usages plus créatifs émergeront certainement avec le temps[3].
Conclusion
Le support des closures en Java était attendu impatiemment. Prévues pour Java 7 à l’origine, elles seront finalement disponibles en Java 8, d
ont la sortie est prévue pour l’année prochaine.
Leur implémentation, sous forme d’expressions lambda, me paraît toutefois très décevante.
Leur domaine d’application, strictement limité à l’implémentation d’interfaces à méthode unique, est en définitive très restreint.
Les changements les plus significatifs sont à attendre du côté des collections, qui gagneront des méthodes empruntées à la programmation fonctionnelle. Pour le reste du code, les gains semblent moins évidents.
Encore une fois, je ne doute pas que la communauté s’emparera de ce nouvel outil, et saura en tirer la quintessence. Vivement Java 8 !
Références
Quelques références utiles :
- Java 8 developer preview
- State of the lambda, par Brian Goetz.
Notes
[1] Une seule méthode propre, c’est-à-dire hors méthodes héritées d’Object, et hors “defender methods”. Par exemple, java.util.Comparable
définit deux “defender methods” en plus de la méthode compare()
, mais est bien considéré comme une “Interface SAM”.
[2] A titre personnel, j’aurais préféré la syntaxe de Groovy. Mais de gustibus non est disputandum…
[3] Voir par exemple l’article de François Sarradin : Towards pattern matching in Java
Merci pour l’article, enfin un texte simple pour comprendre ce sujet.
Il y a un petit mélange entre Filter et Predicate dans le premier exemple Java, et, après avoir cherché les sens du mot volatilité dans le dictionnaire je ne comprends toujours pas le sens de “volatilité des closures”.
Merci pour ces remarques, j’ai corrigé !
Au moins, c’est un premier pas 😉
Reste que, vu ce qu’ils proposent pour le JDK 8, il était quand même possible d’obtenir cela dans le JDK 7. C’est là, je trouve, un des cotés regrettables de la chose.
Fait amusant, je me suis repenché sur ma “next JDK wishlist”. Dans un post mi-2009 – http://www.jroller.com/dmdevito/ent… – j’imaginais dans un premier temps, un genre d’auto-boxing des méthodes statiques pour les transformer en une interface SAM, puis après (dans mon update du 4 juillet 2009), un auto-boxing des lambdas en interfaces SAM (soit, ne pas introduire des delegates dans Java, mais utiliser les SAM).
4 ans (voire plus) pour introduire ce genre d’auto-boxing dans Java, cela fait un peu long…
En même temps, l’auto-boxing des lambdas en SAM a l’air simple coté utilisateur, mais la machinerie interne pour avoir de bonnes perfs a l’air plutôt compliqué ; Fredrik Öhrström indique : “The Java language is getting a lambda operator and a Mount Everest of technical improvements to assist this seemingly simple operator” dans http://www.infoq.com/presentations/…
“effectivement finales, c’est-à-dire qui ne sont pas explicitement déclarées final, mais dont la valeur n’est jamais modifiée après leur première initialisation”
Attention Olivier, dire que “dont la valeur n’est jamais modifiée après leur première initialisation” n’est pas tout à fait exact. On peut la modifier, mais c’est la valeur (référence) assignée au moment de la déclaration du lambda qui est gardée.
J’ai fait quelques expérimentations pour clarifier la chose ici: [http://doanduyhai.wordpress.com/2012/07/12/java-8-lambda-in-details-part-ii-scoping-of-this-and-effectively-final-variable-semantic/]
On apprend au passage quelques renseignements intéressants sur l’implémentation de la capture des variables. Mon petit doigt me dit que ça va donner beaucoup de bugs ce genre de subtilité…
Pour conclure le billet, ça aurait intéressant de voir l’exemple PrintNames traduit en Java8, non?