Beginnen Sie mit Lambda-Ausdrücken in Java

Vor Java SE 8 wurden normalerweise anonyme Klassen verwendet, um Funktionen an eine Methode zu übergeben. Diese Praxis verschleierte den Quellcode und erschwerte das Verständnis. Java 8 beseitigte dieses Problem durch die Einführung von Lambdas. In diesem Lernprogramm wird zunächst die Lambda-Sprachfunktion vorgestellt und anschließend eine detailliertere Einführung in die funktionale Programmierung mit Lambda-Ausdrücken und Zieltypen gegeben. Außerdem erfahren Sie , wie Lambda - Ausdrücke mit Bereichen, lokale Variablen interagieren, die thisund superSchlüsselwörter und Java Ausnahmen. 

Beachten Sie, dass Codebeispiele in diesem Lernprogramm mit JDK 12 kompatibel sind.

Typen selbst entdecken

Ich werde in diesem Tutorial keine Nicht-Lambda-Sprachfunktionen vorstellen, die Sie zuvor noch nicht kennengelernt haben, aber ich werde Lambdas anhand von Typen demonstrieren, die ich zuvor in dieser Serie nicht besprochen habe. Ein Beispiel ist die java.lang.MathKlasse. Ich werde diese Typen in zukünftigen Java 101-Tutorials vorstellen. Im Moment empfehle ich, die JDK 12-API-Dokumentation zu lesen, um mehr darüber zu erfahren.

download Code abrufen Laden Sie den Quellcode herunter, zum Beispiel Anwendungen in diesem Tutorial. Erstellt von Jeff Friesen für JavaWorld.

Lambdas: Eine Grundierung

Ein Lambda-Ausdruck (Lambda) beschreibt einen Codeblock (eine anonyme Funktion), der zur späteren Ausführung an Konstruktoren oder Methoden übergeben werden kann. Der Konstruktor oder die Methode erhält das Lambda als Argument. Betrachten Sie das folgende Beispiel:

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

In diesem Beispiel wird ein Lambda für die Ausgabe einer Nachricht an den Standardausgabestream angegeben. ()Identifiziert von links nach rechts die formale Parameterliste des Lambdas (im Beispiel gibt es keine Parameter), ->gibt an, dass der Ausdruck ein Lambda ist und System.out.println("Hello")der auszuführende Code ist.

Lambdas vereinfachen die Verwendung von funktionalen Schnittstellen , bei denen es sich um kommentierte Schnittstellen handelt, die jeweils genau eine abstrakte Methode deklarieren (obwohl sie auch eine beliebige Kombination von Standard-, statischen und privaten Methoden deklarieren können). Beispielsweise bietet die Standardklassenbibliothek eine java.lang.RunnableSchnittstelle mit einer einzelnen abstrakten void run()Methode. Die Deklaration dieser Funktionsschnittstelle wird unten angezeigt:

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

Die Klassenbibliothek kommentiert Runnablemit @FunctionalInterface, was eine Instanz des java.lang.FunctionalInterfaceAnnotationstyps ist. FunctionalInterfacewird verwendet, um die Schnittstellen zu kommentieren, die in Lambda-Kontexten verwendet werden sollen.

Ein Lambda hat keinen expliziten Schnittstellentyp. Stattdessen verwendet der Compiler den umgebenden Kontext, um abzuleiten, welche funktionale Schnittstelle instanziiert werden soll, wenn ein Lambda angegeben wird - das Lambda ist an diese Schnittstelle gebunden . Angenommen, ich habe das folgende Codefragment angegeben, das das vorherige Lambda als Argument an den Konstruktor der java.lang.ThreadKlasse übergibt Thread(Runnable target):

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

Der Compiler stellt fest, dass das Lambda übergeben wird, Thread(Runnable r)da dies der einzige Konstruktor ist, der das Lambda erfüllt: Es Runnablehandelt sich um eine funktionale Schnittstelle, die leere formale Parameterliste des Lambda ()stimmt mit run()der leeren Parameterliste voidüberein , und die Rückgabetypen ( ) stimmen ebenfalls überein. Das Lambda ist gebunden an Runnable.

Listing 1 zeigt den Quellcode einer kleinen Anwendung, mit der Sie mit diesem Beispiel spielen können.

Listing 1. LambdaDemo.java (Version 1)

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

Kompilieren Sie Listing 1 ( javac LambdaDemo.java) und führen Sie die Anwendung ( java LambdaDemo) aus. Sie sollten die folgende Ausgabe beachten:

Hello

Lambdas kann die Menge an Quellcode, die Sie schreiben müssen, erheblich vereinfachen und das Verständnis von Quellcode erheblich vereinfachen. Ohne Lambdas würden Sie beispielsweise wahrscheinlich den ausführlicheren Code von Listing 2 angeben, der auf einer Instanz einer anonymen Klasse basiert, die implementiert wird 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(); } }

Führen Sie nach dem Kompilieren dieses Quellcodes die Anwendung aus. Sie werden dieselbe Ausgabe wie zuvor sehen.

Lambdas und die Streams-API

Lambdas vereinfachen nicht nur den Quellcode, sondern spielen auch eine wichtige Rolle in der funktionsorientierten Streams-API von Java. Sie beschreiben Funktionseinheiten, die an verschiedene API-Methoden übergeben werden.

Java Lambdas in der Tiefe

Um Lambdas effektiv nutzen zu können, müssen Sie die Syntax von Lambda-Ausdrücken zusammen mit dem Begriff eines Zieltyps verstehen. Sie müssen auch , wie lambdas interact mit Bereichen, lokalen Variablen zu verstehen, die thisund superKeywords und Ausnahmen. Ich werde all diese Themen in den folgenden Abschnitten behandeln.

Wie Lambdas umgesetzt werden

Lambdas werden in Bezug auf die invokedynamicAnweisung der virtuellen Java-Maschine und die java.lang.invokeAPI implementiert . Sehen Sie sich das Video Lambda: Ein Blick unter die Haube an, um mehr über die Lambda-Architektur zu erfahren.

Lambda-Syntax

Jedes Lambda entspricht der folgenden Syntax:

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

Dies formal-parameter-listist eine durch Kommas getrennte Liste formaler Parameter, die zur Laufzeit mit den Parametern der einzelnen abstrakten Methode einer Funktionsschnittstelle übereinstimmen müssen. Wenn Sie ihre Typen weglassen, leitet der Compiler diese Typen aus dem Kontext ab, in dem das Lambda verwendet wird. Betrachten Sie die folgenden Beispiele:

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

Lambdas und var

Ab Java SE 11 können Sie einen Typnamen durch ersetzen var. Zum Beispiel könnten Sie angeben (var a, var b).

Sie müssen Klammern für mehrere oder keine formalen Parameter angeben. Sie können jedoch die Klammern weglassen (obwohl dies nicht erforderlich ist), wenn Sie einen einzelnen formalen Parameter angeben. (Dies gilt nur für den Parameternamen. Wenn der Typ ebenfalls angegeben wird, sind Klammern erforderlich.) Beachten Sie die folgenden zusätzlichen Beispiele:

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); } }