Premiers pas avec les expressions lambda en Java

Avant Java SE 8, les classes anonymes étaient généralement utilisées pour transmettre des fonctionnalités à une méthode. Cette pratique obscurcit le code source, ce qui le rend plus difficile à comprendre. Java 8 a éliminé ce problème en introduisant des lambdas. Ce didacticiel présente d'abord la fonctionnalité de langage lambda, puis fournit une introduction plus détaillée à la programmation fonctionnelle avec des expressions lambda ainsi que des types de cibles. Vous apprendrez également comment les lambdas interagissent avec les étendues, les variables locales, les mots this- superclés et et les exceptions Java. 

Notez que les exemples de code de ce didacticiel sont compatibles avec JDK 12.

Découvrir les types par vous-même

Je n'introduirai aucune fonctionnalité de langage non lambda dans ce didacticiel dont vous n'avez pas encore entendu parler, mais je vais démontrer les lambdas via des types dont je n'ai pas encore parlé dans cette série. Un exemple est la java.lang.Mathclasse. Je présenterai ces types dans les futurs didacticiels Java 101. Pour l'instant, je suggère de lire la documentation de l'API JDK 12 pour en savoir plus à leur sujet.

télécharger Obtenir le code Téléchargez le code source des exemples d'applications dans ce didacticiel. Créé par Jeff Friesen pour JavaWorld.

Lambdas: une amorce

Une expression lambda (lambda) décrit un bloc de code (une fonction anonyme) qui peut être passé aux constructeurs ou aux méthodes pour une exécution ultérieure. Le constructeur ou la méthode reçoit le lambda comme argument. Prenons l'exemple suivant:

() -> System.out.println("Hello")

Cet exemple identifie un lambda pour la sortie d'un message vers le flux de sortie standard. De gauche à droite, ()identifie la liste des paramètres formels du lambda (il n'y a pas de paramètres dans l'exemple), ->indique que l'expression est un lambda et System.out.println("Hello")est le code à exécuter.

Lambdas simplifie l'utilisation des interfaces fonctionnelles , qui sont des interfaces annotées qui déclarent chacune exactement une méthode abstraite (bien qu'elles puissent également déclarer toute combinaison de méthodes par défaut, statiques et privées). Par exemple, la bibliothèque de classes standard fournit une java.lang.Runnableinterface avec une seule void run()méthode abstraite . La déclaration de cette interface fonctionnelle apparaît ci-dessous:

@FunctionalInterface public interface Runnable { public abstract void run(); }

La bibliothèque de classes annote Runnableavec @FunctionalInterface, qui est une instance du java.lang.FunctionalInterfacetype d'annotation. FunctionalInterfaceest utilisé pour annoter les interfaces qui doivent être utilisées dans des contextes lambda.

Un lambda n'a pas de type d'interface explicite. Au lieu de cela, le compilateur utilise le contexte environnant pour déduire quelle interface fonctionnelle instancier lorsqu'un lambda est spécifié - le lambda est lié à cette interface. Par exemple, supposons que je spécifie le fragment de code suivant, qui transmet le lambda précédent en tant qu'argument au constructeur de la java.lang.Threadclasse Thread(Runnable target):

new Thread(() -> System.out.println("Hello"));

Le compilateur détermine que le lambda est passé Thread(Runnable r)car c'est le seul constructeur qui satisfait le lambda: Runnableest une interface fonctionnelle, la liste de paramètres formels vide du lambda ()correspond à run()la liste de paramètres vide de, et les types de retour ( void) sont également d'accord. Le lambda est lié à Runnable.

Le listing 1 présente le code source à une petite application qui vous permet de jouer avec cet exemple.

Listing 1. LambdaDemo.java (version 1)

public class LambdaDemo { public static void main(String[] args) { new Thread(() -> System.out.println("Hello")).start(); } }

Compilez le listing 1 ( javac LambdaDemo.java) et exécutez l'application ( java LambdaDemo). Vous devez observer la sortie suivante:

Hello

Lambdas peut considérablement simplifier la quantité de code source que vous devez écrire et peut également rendre le code source beaucoup plus facile à comprendre. Par exemple, sans lambdas, vous spécifieriez probablement le code plus détaillé du Listing 2, qui est basé sur une instance d'une classe anonyme qui implémente Runnable.

Listing 2. LambdaDemo.java (version 2)

public class LambdaDemo { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello"); } }; new Thread(r).start(); } }

Après avoir compilé ce code source, exécutez l'application. Vous découvrirez la même sortie que celle indiquée précédemment.

Lambdas et l'API Streams

En plus de simplifier le code source, les lambdas jouent un rôle important dans l'API Streams à orientation fonctionnelle de Java. Ils décrivent des unités de fonctionnalité qui sont transmises à diverses méthodes d'API.

Java lambdas en profondeur

Pour utiliser efficacement lambdas, vous devez comprendre la syntaxe des expressions lambda ainsi que la notion de type cible. Vous devez également comprendre comment les lambdas interagissent avec les étendues, les variables locales, les mots this- superclés et et les exceptions. Je couvrirai tous ces sujets dans les sections qui suivent.

Comment les lambdas sont implémentées

Les lambdas sont implémentés en termes d' invokedynamicinstructions de la machine virtuelle Java et de l' java.lang.invokeAPI. Regardez la vidéo Lambda: un coup d'oeil sous le capot pour en savoir plus sur l'architecture lambda.

Syntaxe Lambda

Chaque lambda est conforme à la syntaxe suivante:

( formal-parameter-list ) -> { expression-or-statements }

La formal-parameter-listest une liste séparée par des virgules des paramètres formels qui doivent correspondre aux paramètres de méthode unique abstraite d'une interface fonctionnelle lors de l' exécution. Si vous omettez leurs types, le compilateur déduit ces types du contexte dans lequel le lambda est utilisé. Considérez les exemples suivants:

(double a, double b) // types explicitly specified (a, b) // types inferred by compiler

Lambdas et var

À partir de Java SE 11, vous pouvez remplacer un nom de type par var. Par exemple, vous pouvez spécifier (var a, var b).

Vous devez spécifier des parenthèses pour plusieurs paramètres ou aucun paramètre formel. Cependant, vous pouvez omettre les parenthèses (bien que ce ne soit pas obligatoire) lorsque vous spécifiez un seul paramètre formel. (Cela s'applique uniquement au nom du paramètre - les parenthèses sont obligatoires lorsque le type est également spécifié.) Considérez les exemples supplémentaires suivants:

x // parentheses omitted due to single formal parameter (double x) // parentheses required because type is also present () // parentheses required when no formal parameters (x, y) // parentheses required because of multiple formal parameters

The formal-parameter-list is followed by a -> token, which is followed by expression-or-statements--an expression or a block of statements (either is known as the lambda's body). Unlike expression-based bodies, statement-based bodies must be placed between open ({) and close (}) brace characters:

(double radius) -> Math.PI * radius * radius radius -> { return Math.PI * radius * radius; } radius -> { System.out.println(radius); return Math.PI * radius * radius; }

The first example's expression-based lambda body doesn't have to be placed between braces. The second example converts the expression-based body to a statement-based body, in which return must be specified to return the expression's value. The final example demonstrates multiple statements and cannot be expressed without the braces.

Lambda bodies and semicolons

Note the absence or presence of semicolons (;) in the previous examples. In each case, the lambda body isn't terminated with a semicolon because the lambda isn't a statement. However, within a statement-based lambda body, each statement must be terminated with a semicolon.

Listing 3 presents a simple application that demonstrates lambda syntax; note that this listing builds on the previous two code examples.

Listing 3. LambdaDemo.java (version 3)

@FunctionalInterface interface BinaryCalculator { double calculate(double value1, double value2); } @FunctionalInterface interface UnaryCalculator { double calculate(double value); } public class LambdaDemo { public static void main(String[] args) { System.out.printf("18 + 36.5 = %f%n", calculate((double v1, double v2) -> v1 + v2, 18, 36.5)); System.out.printf("89 / 2.9 = %f%n", calculate((v1, v2) -> v1 / v2, 89, 2.9)); System.out.printf("-89 = %f%n", calculate(v -> -v, 89)); System.out.printf("18 * 18 = %f%n", calculate((double v) -> v * v, 18)); } static double calculate(BinaryCalculator calc, double v1, double v2) { return calc.calculate(v1, v2); } static double calculate(UnaryCalculator calc, double v) { return calc.calculate(v); } }

Listing 3 first introduces the BinaryCalculator and UnaryCalculator functional interfaces whose calculate() methods perform calculations on two input arguments or on a single input argument, respectively. This listing also introduces a LambdaDemo class whose main() method demonstrates these functional interfaces.

The functional interfaces are demonstrated in the static double calculate(BinaryCalculator calc, double v1, double v2) and static double calculate(UnaryCalculator calc, double v) methods. The lambdas pass code as data to these methods, which are received as BinaryCalculator or UnaryCalculator instances.

Compile Listing 3 and run the application. You should observe the following output:

18 + 36.5 = 54.500000 89 / 2.9 = 30.689655 -89 = -89.000000 18 * 18 = 324.000000

Target types

A lambda is associated with an implicit target type, which identifies the type of object to which a lambda is bound. The target type must be a functional interface that's inferred from the context, which limits lambdas to appearing in the following contexts:

  • Variable declaration
  • Assignment
  • Return statement
  • Array initializer
  • Method or constructor arguments
  • Lambda body
  • Ternary conditional expression
  • Cast expression

Listing 4 presents an application that demonstrates these target type contexts.

Listing 4. LambdaDemo.java (version 4)

import java.io.File; import java.io.FileFilter; import java.nio.file.Files; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitor; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; public class LambdaDemo { public static void main(String[] args) throws Exception { // Target type #1: variable declaration Runnable r = () -> { System.out.println("running"); }; r.run(); // Target type #2: assignment r = () -> System.out.println("running"); r.run(); // Target type #3: return statement (in getFilter()) File[] files = new File(".").listFiles(getFilter("txt")); for (int i = 0; i  path.toString().endsWith("txt"), (path) -> path.toString().endsWith("java") }; FileVisitor visitor; visitor = new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) { Path name = file.getFileName(); for (int i = 0; i  System.out.println("running")).start(); // Target type #6: lambda body (a nested lambda) Callable callable = () -> () -> System.out.println("called"); callable.call().run(); // Target type #7: ternary conditional expression boolean ascendingSort = false; Comparator cmp; cmp = (ascendingSort) ? (s1, s2) -> s1.compareTo(s2) : (s1, s2) -> s2.compareTo(s1); List cities = Arrays.asList("Washington", "London", "Rome", "Berlin", "Jerusalem", "Ottawa", "Sydney", "Moscow"); Collections.sort(cities, cmp); for (int i = 0; i < cities.size(); i++) System.out.println(cities.get(i)); // Target type #8: cast expression String user = AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("user.name")); System.out.println(user); } static FileFilter getFilter(String ext) { return (pathname) -> pathname.toString().endsWith(ext); } }