Accès rapide :
La vidéo
Utilisation d'expressions lambda
Interface fonctionnelle
Qu'est-ce qu'une expression lambda en Java ?
Syntaxe de définition d'expressions lambda
Travaux pratiques
Le sujet
La correction
Les lambdas et la capture de l'environnement de définition
Les références sur méthodes
Un premier exemple appliqué à la gestion des événements
Un second exemple appliqué à la manipulation de collections
Travaux pratiques
Le sujet
La correction
Cette nouvelle vidéo vous présente deux notions introduites à partir du Java SE 8.0 : les expressions lambdas et les références sur méthodes. Des exemples appliqués à la gestion des événements et à la gestion des collections vous sont proposés.
Malgré ce que laisse penser le terme, une expression lambda n'est pas une expression (en tout cas pas en Java). En Java, on utilise une expression lambda pour nous faciliter la vie en cas de besoin d'implémentation d'une interface possédant une seule et unique méthode abstraite. Une expression lambda est donc en lien avec le concept d'interface, que nous avons déjà bien appréhendé dans le chapitre précédent.
Le fonctionnement des expressions lambda est spécifié dans la JSR 335 (Project Lambda). Cette JSR fait partie de la spécification Java SE 8.0.
Nous appellerons une interface possédant une seule méthode abstraite une « interface fonctionnelle ».
Le Java SE 8.0 intègre une annotation permettant de marquer de telles interfaces : cette annotation est de type java.lang.FunctionalInterface
.
De nombreuses interfaces déjà existantes dans les versions précédentes de Java ont été marquées via cette annotation : c'est le cas, par exemple, de
l'interface java.lang.Runnable
souvent utilisée par vos Threads.
1 2 3 4 5 6 7 8 |
package java.lang; @FunctionalInterface public interface Runnable { void run(); } |
D'autres interfaces fonctionnelles ont été rajoutées depuis la version 8.0 du Java SE : par exemple, l'interface fonctionnelle
java.util.function.Predicate<T>
. Dans ce cas précis, une seule méthode abstraite existe sur l'interface : la méthode
boolean test(T t)
. Par contre, cette interface expose un certain nombre d'autres méthodes non abstraites
(des « default methods » et des méthodes statiques). Nous reviendrons sur cette interface plus tard dans ce document.
En Java, une expression lambda est une forme de syntaxe permettant de simplifier l'implémentation d'une interface fonctionnelle (une interface à une seule et unique méthode). On peut simplifier la chose en affirmant que cette syntaxe est du « sucre syntaxique » pour la production d'une classe anonyme : on entend par là, qu'elle simplifie la mise en oeuvre d'une classe anonyme.
Comme vous allez le constater, cette nouvelle syntaxe est beaucoup plus compacte. Pour produire la classe anonyme, le compilateur va devoir faire de l'inférence de type (de la déduction de type) à partir des éléments présents dans votre code.
Avant de vous présenter la syntaxe de définition d'une expression lambda, reprenons l'exemple d'un gestionnaire d'événements pour le clic sur un bouton. Comme nous l'avons vu dans le chapitre précédent, plusieurs possibilités d'implémentations sont possibles. Je vous rappelle ici celle à base d'une classe anonyme.
1 2 3 4 5 |
ActionListener listener = new ActionListener() { public void actionPerformed( ActionEvent event ) { System.out.println( "Button clicked" ); } }; |
actionPerformed
.
C'est donc bien une FunctionalInterface
.
Ce qu'il faut comprendre, c'est que votre IDE peut vous aider à produire une partie du code d'une classe anonyme.
Pour valider ce point avec Eclipse, commencez par écrire le code suivant : ActionListener listener = new
(n'oubliez pas l'espace final),
puis enclenchez la séquence de touches CTRL + SPACE : votre IDE doit normalement produire la suite du code et notamment le squelette de la méthode
actionPerformed
.
Mais comment fait-il ? Et bien, il fait lui aussi de l'inférence de type. La variable listener
étant typée via ActionListener
,
il en déduit qu'il faut générer un squelette pour la méthode actionPerformed
.
Ceux qui ont proposé la syntaxe des expressions lambda en sont arrivés à la conclusion suivante : si votre IDE peut produire une partie du code, pourquoi le compilateur ne pourrait pas en faire autant et vous décharger ainsi d'une partie du code ?
On reconnaît une expression lambda grâce à l'utilisation de l'opérateur ->
. Malgré cela, plusieurs variations dans la syntaxe
sont possibles. Les quatre exemples suivant doivent normalement parfaitement compiler (à condition d'utiliser un Java SE 8.0 ou supérieur).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// La forme la plus compacte (uniquement si vous n'avez qu'un seul paramètre). // Le type du paramètre sera déduit de la signature de la méthode à redéfinir. // Une seule instruction doit être placée à la suite de l'opérateur -> ActionListener listener1 = e -> System.out.println( "Button clicked" ); // Ici, le paramètre est placé entre parenthèses. // Vous pouvez définir plusieurs paramètres séparés par des virgules. ActionListener listener2 = (e) -> System.out.println( "Button clicked" ); // Si vous préférez vous pouvez typer le paramètre. ActionListener listener3 = (ActionEvent e) -> System.out.println( "Button clicked" ); // Enfin, si vous avez plusieurs instructions à placer dans la lambda, // mettez les entre accolades. ActionListener listener4 = (ActionEvent e) -> { System.out.println( "Button clicked" ); }; |
Voici quelques explications complémentaires sur ces quatre syntaxes de définition d'expressions lambda.
La première forme est la plus compacte mais doit être utilisée sous certaines conditions. L'absence de parenthèses autour du paramètre n'est
possible que si vous vous n'avez qu'un unique paramètre et que vous ne cherchiez pas à le typer. L'absence d'accolade autour du corps de la
lambda n'est possible que et uniquement que si vous n'avez qu'une unique instruction à exécuter. Dans ce cas, si vous devez retourner une
valeur, il ne faut pas mettre le mot clé return
. Notez aussi qu'il ne faut pas mettre de ;
pour terminer l'unique
instruction du bloc.
La seconde forme vous montre que vous pouvez entourer la liste des paramètres de la lambda avec des parenthèses. Personnellement, j'ai tendance à les mettre systématiquement. Elles sont obligatoires si vous avez au moins deux paramètres ou si vous cherchez à typer vos paramètres.
Cette troisième forme vous montre comment typer un ou plusieurs paramètres. Dans ce cas, l'emploi des parenthèses est obligatoire.
Enfin, la dernière forme vous montre que l'on peut borner le corps de la lambda avec une paire d'accolades. Cela est nécessaire si votre lambda
doit exécuter plus qu'une instruction. Dans ce cas, chaque instruction doit, bien naturellement, être terminée avec un caractère ;
.
Si vous devez retourner une valeur de sortie, l'utilisation du mot clé return
est obligatoire, comme dans le cas d'une implémentation
traditionnelle de votre interface fonctionnelle.
L'exemple suivant reprend le code vu dans le chapitre précédent qui créé une interface graphique et lui ajoute des gestionnaires d'événements. Les classes anonymes sont remplacées par des expressions lambda.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package fr.koor.poo; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.plaf.nimbus.NimbusLookAndFeel; public class Demo extends JFrame { private static final long serialVersionUID = -4939544011287453046L; private JButton btnClickMe = new JButton( "Click me!" ); private JButton btnPushMe = new JButton( "Push me!" ); private JButton btnActivateMe = new JButton( "Activate me!" ); public Demo() { super( "Implémentation d'interface" ); this.setDefaultCloseOperation( DISPOSE_ON_CLOSE ); JPanel contentPane = (JPanel) this.getContentPane(); contentPane.setLayout( new FlowLayout() ); contentPane.add( btnClickMe ); contentPane.add( btnPushMe ); contentPane.add( btnActivateMe ); btnClickMe.addActionListener( e -> btnActivateMe.setText( "First button clicked!" + e.getSource() ) ); btnPushMe.addActionListener( (e) -> System.out.println( "btnPushMe clicked" + e.getSource() ) ); btnActivateMe.addActionListener( (e) -> { // On change le titre de la fenêtre ! setTitle( "Button clicked" + e.getSource() ); } ); this.setSize( 400, 200 ); this.setLocationRelativeTo( null ); } public static void main( String[] args ) throws Exception { // Try to set Nimbus look and feel UIManager.setLookAndFeel( new NimbusLookAndFeel() ); // Start the demo Demo demo = new Demo(); demo.setVisible( true ); } } |
Nous souhaitons trier une collection de chaînes de caractères en étant « case insensitive » (non sensible à la casse (minuscules/majuscules)).
L'interface java.util.List
expose une méthode sort
acceptant un comparateur en paramètres. Ce comparateur doit implémenter
l'interface java.util.Comparator<T>
(un type générique, nous reviendrons sur ce sujet ultérieurement).
Voici un exemple de code réalisant ce tri en utilisant une implémentation de l'interface Comparator
produite par une classe anonyme.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class Sample { public static void main( String [] args ) { List<String> collection = new ArrayList<>(); collection.add( "Java" ); collection.add( "c" ); collection.add( "Python" ); collection.add( "C++" ); collection.add( "ada" ); collection.add( "lisp" ); collection.sort( new Comparator<String>() { @Override public int compare( String l1, String l2 ) { return l1.compareToIgnoreCase( l2 ); } } ); for ( String language : collection ) { System.out.println( language ); } } } |
Le but de l'exercice est de réécrire ce code en remplaçant la classe anonyme par une expression lambda. A vous de faire, mais attention, on regarde la correction après avoir fait l'exercice ;-).
Voici donc le même programme réécrit pour utiliser une expression lambda en lieu et place de la classe anonyme.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import java.util.ArrayList; import java.util.List; public class Sample { public static void main( String [] args ) { List<String> collection = new ArrayList<>(); collection.add( "Java" ); collection.add( "c" ); collection.add( "Python" ); collection.add( "C++" ); collection.add( "ada" ); collection.add( "lisp" ); collection.sort( (l1, l2) -> l1.compareToIgnoreCase( l2 ) ); for ( String language : collection ) { System.out.println( language ); } } } |
Comparator
, son importation n'est plus requise.
Maintenant, passons aux choses sérieuses : les expressions lambda capturent leur environnement de définition.
Mais qu'est-ce que cela veut bien dire ? Pour comprendre la chose regardons attentivement l'exemple ci-dessous : il définit deux expressions
lambda (toutes les deux produites par la méthode createLambda
) qui seront invoquées après coup.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@FunctionalInterface interface Demo { public int doSomething( int param ); } public class Sample { public static Demo createLambda( int factoryParam ) { int localVariable = 10; return (lambdaParam) -> localVariable + factoryParam + lambdaParam; } public static void main( String [] args ) throws Exception { Demo l1 = createLambda( 100 ); Demo l2 = createLambda( 200 ); Thread.sleep( 1000 ); System.out.println( l1.doSomething( 1000 ) ); System.out.println( l2.doSomething( 2000 ) ); } } |
Ce qui peut sembler fou dans cet exemple, c'est que la lambda, définie en ligne 10, utilise les valeurs d'une variable locale et un paramètre
après être sorti de l'appel à la méthode createLambda
, qui définit ces deux éléments. Or, la durée de vie d'un paramètre ou d'une
variable locale, ne serait-elle pas le temps d'exécution de la méthode ? Comment la lambda peut-elle compiler ?
Et bien, c'est là qu'intervient le concept de capture. Lors de la création de la lambda, elle réalise une capture de ce à quoi elle a accès lors de
sa définition. Par la suite, elle peut s'en resservir. Du coup, les résultats affichés par ce programme sont bien 1110
puis 2210
.
En fait, cette possibilité était déjà bien présente dans le Java SE 7.0. A l'époque, au lieu d'utiliser une lambda, on définissait une classe anonyme.
Cette classe anonyme pouvait déjà utiliser les valeurs de paramètres et de variables locales à condition de les avoir marqués comme étant finaux, avec
le mot final
. Voici le même programme que précédemment, mais réalisé avec une classe anonyme et de la finalisation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@FunctionalInterface interface Demo { public int doSomething( int param ); } public class Sample { public static Demo createLambda( final int factoryParam ) { final int localVariable = 10; return new Demo() { @Override public int doSomething( int param ) { return localVariable + factoryParam + param; } }; } public static void main( String [] args ) throws Exception { Demo l1 = createLambda( 100 ); Demo l2 = createLambda( 200 ); Thread.sleep( 1000 ); System.out.println( l1.doSomething( 1000 ) ); System.out.println( l2.doSomething( 2000 ) ); } } |
Du coup, on peut en déduire une affirmation : une lambda ne peut en aucun cas modifier les éléments (paramètres ou variables locales) qu'elle a capturé, car ils ont automatiquement finalisés. Si vous cherchez à modifier un des éléments capturés, une erreur de compilation sera systématiquement produite, comme en atteste l'exemple suivant.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@FunctionalInterface interface Demo { public int doSomething( int param ); } public class Sample { public static Demo createLambda( int factoryParam ) { int localVariable = 10; return (lambdaParam) -> { factoryParam++; return localVariable + factoryParam + lambdaParam; }; } public static void main( String [] args ) throws Exception { Demo l1 = createLambda( 100 ); Demo l2 = createLambda( 200 ); Thread.sleep( 1000 ); System.out.println( l1.doSomething( 1000 ) ); System.out.println( l2.doSomething( 2000 ) ); } } |
Et voici les résultats produits par le compilateur Java :
$> javac Sample.java Sample.java:11: error: local variables referenced from a lambda expression must be final or effectively final factoryParam++; ^ Sample.java:12: error: local variables referenced from a lambda expression must be final or effectively final return localVariable + factoryParam + lambdaParam; ^ 2 errors $>
Pour clore ce chapitre, je tenais à vous présenter le concept de référence sur méthode. Ce concept a été ajouté à Java SE, à partir de sa version 8.0. Il est en lien direct avec les lambas et les classes anonymes dans le sens ou une référence sur méthode, en Java, correspond à une implémentation automatique d'une classe anonyme et à un renvoi sur une méthode de votre choix.
Considérons la méthode suivante : on part d'un principe qu'elle sera associée à un gestionnaire d'événements sur un bouton Swing.
1 2 3 |
private void btnPushMeListener( ActionEvent event ) { System.out.println( "btnPushMe clicked" + e.getSource() ); } |
Vous pouvez alors enregistrer un listener sur le bouton qui va automatiquement renvoyer vers la méthode btnPushMeListener
.
On connaît déjà au moins deux manières de réaliser cet enregistrement d'écouteur. Soit via une classe anonyme :
1 2 3 4 5 6 |
btnPushMe.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent event ) { btnPushMeListener( event ); // On renvoit sur la méthode dédiée. } } ); |
Soit via une expression lambda :
1 |
btnPushMe.addActionListener( (event) -> btnPushMeListener( event ) ); |
Et bien, nous avons maintenant une nouvelle possibilité équivalente : l'utilisation d'une référence sur méthode.
Une référence sur méthode s'introduit par l'opérateur ::
, mais il faut bien comprendre qu'on produit une instance (un objet) de type
ActionListener
(en tout cas, dans notre exemple).
1 |
btnPushMe.addActionListener( this::btnPushMeListener );
|
ActionListener
) car on doit relayer l'appel sur cette méthode. Si la signature de votre
méthode n'est pas compatible avec ce qui est attendu, une erreur de compilation sera produite.
Je peux vous proposer un autre exemple, imaginons que l'on souhaite filtrer dans une collection de chaînes de caractères, toutes celles qui commencent
par une lettre J
(en majuscule). Java permet de traiter en lot les éléments d'une collection via la notion de streams.
Pour acquérir un stream à partir d'une collection, il faut utiliser la méthode stream()
. Une fois un stream obtenu, on peut filtrer ses
éléments. Pour ce faire, il faut invoquer la méthode filter
sur le stream : cette méthode accepte en paramètre une instance typée
via l'interface java.util.function.Predicate<T>
. Cette interface définit notamment la méthode abstraite test
:
1 2 3 4 5 6 7 8 |
@FunctionalInterface public interface Predicate<T> { public boolean test(T t); // Suite de l'interface (des méthodes statiques et des "default" méthodes). } |
Il est donc possible d'utiliser les références sur méthodes pour renvoyer les tests sur une méthode statique de votre classe, comme le montre l'exemple ci-dessous.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import java.util.ArrayList; import java.util.List; public class Sample { private static boolean filter( String language ) { return language.charAt( 0 ) == 'J'; } public static void main( String [] args ) { List<String> collection = new ArrayList<>(); collection.add( "Java" ); collection.add( "c" ); collection.add( "Jython" ); collection.add( "C++" ); collection.add( "ada" ); collection.add( "lisp" ); collection.stream() .filter( Sample::filter ) // Référence sur une méthode statique .forEach( System.out::println ); // Référence sur une méthode d'instance } } |
forEach
pour passer toutes les chaînes de caractères sélectionnées
à la méthode System.out.println
.
L'exécution de ce programme affiche plus que deux lignes : un pour le langage Java et l'autre pour Jython. Pour information, Jython est un environnement permettant d'utiliser les APIs Java au travers du langage Python. Au final, un programme Jython s'exécute dans une JVM.
Reprendre le programme suivant et le recoder pour qu'il utilise des références sur méthode en lieu et place des trois expressions lambdas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package fr.koor.poo; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.plaf.nimbus.NimbusLookAndFeel; public class Demo extends JFrame { private static final long serialVersionUID = -4939544011287453046L; private JButton btnClickMe = new JButton( "Click me!" ); private JButton btnPushMe = new JButton( "Push me!" ); private JButton btnActivateMe = new JButton( "Activate me!" ); public Demo() { super( "Implémentation d'interface" ); this.setDefaultCloseOperation( DISPOSE_ON_CLOSE ); JPanel contentPane = (JPanel) this.getContentPane(); contentPane.setLayout( new FlowLayout() ); contentPane.add( btnClickMe ); contentPane.add( btnPushMe ); contentPane.add( btnActivateMe ); btnClickMe.addActionListener( e -> btnActivateMe.setText( "First button clicked!" + e.getSource() ) ); btnPushMe.addActionListener( (e) -> System.out.println( "btnPushMe clicked" + e.getSource() ) ); btnActivateMe.addActionListener( (e) -> { // On change le titre de la fenêtre ! setTitle( "Button clicked" + e.getSource() ); } ); this.setSize( 400, 200 ); this.setLocationRelativeTo( null ); } public static void main( String[] args ) throws Exception { // Try to set Nimbus look and feel UIManager.setLookAndFeel( new NimbusLookAndFeel() ); // Start the demo Demo demo = new Demo(); demo.setVisible( true ); } } |
Voici le même programme réécrit pour utiliser des références sur méthodes en lieu et place des expressions lambdas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package fr.koor.poo; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.plaf.nimbus.NimbusLookAndFeel; public class Demo extends JFrame { private static final long serialVersionUID = -4939544011287453046L; private JButton btnClickMe = new JButton( "Click me!" ); private JButton btnPushMe = new JButton( "Push me!" ); private JButton btnActivateMe = new JButton( "Activate me!" ); public Demo() { super( "Implémentation d'interface" ); this.setDefaultCloseOperation( DISPOSE_ON_CLOSE ); JPanel contentPane = (JPanel) this.getContentPane(); contentPane.setLayout( new FlowLayout() ); contentPane.add( btnClickMe ); contentPane.add( btnPushMe ); contentPane.add( btnActivateMe ); btnClickMe.addActionListener( this::btnClickMeListener ); btnPushMe.addActionListener( this::btnPushMeListener ); btnActivateMe.addActionListener( this::btnActivateMeListener ); this.setSize( 400, 200 ); this.setLocationRelativeTo( null ); } private void btnClickMeListener( ActionEvent e ) { btnActivateMe.setText( "First button clicked!" + e.getSource() ); } private void btnPushMeListener( ActionEvent e ) { System.out.println( "btnPushMe clicked" + e.getSource() ); } private void btnActivateMeListener( ActionEvent e ) { // On change le titre de la fenêtre ! setTitle( "Button clicked" + e.getSource() ); } public static void main( String[] args ) throws Exception { // Try to set Nimbus look and feel UIManager.setLookAndFeel( new NimbusLookAndFeel() ); // Start the demo Demo demo = new Demo(); demo.setVisible( true ); } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :