Ga aan de slag met lambda-expressies in Java

Vóór Java SE 8 werden doorgaans anonieme klassen gebruikt om functionaliteit door te geven aan een methode. Deze praktijk versluierde de broncode, waardoor het moeilijker te begrijpen was. Java 8 loste dit probleem op door lambda's te introduceren. Deze tutorial introduceert eerst de lambda-taalfunctie en biedt vervolgens een meer gedetailleerde inleiding tot functioneel programmeren met lambda-expressies samen met doeltypes. U leert ook hoe lambda interactie met scopes, lokale variabelen, de thisen superzoekwoorden, en Java uitzonderingen. 

Merk op dat codevoorbeelden in deze tutorial compatibel zijn met JDK 12.

Zelf typen ontdekken

Ik zal in deze tutorial geen niet-lambda-taalfuncties introduceren die je nog niet eerder hebt geleerd, maar ik zal lambda's demonstreren via typen die ik niet eerder in deze serie heb besproken. Een voorbeeld is de java.lang.Mathklas. Ik zal deze typen introduceren in toekomstige Java 101-tutorials. Voor nu raad ik aan om de JDK 12 API-documentatie te lezen om er meer over te weten te komen.

download Download de code Download de broncode voor voorbeeldtoepassingen in deze tutorial. Gemaakt door Jeff Friesen voor JavaWorld.

Lambdas: een primer

Een lambda-expressie (lambda) beschrijft een codeblok (een anonieme functie) dat kan worden doorgegeven aan constructeurs of methoden voor latere uitvoering. De constructor of methode ontvangt de lambda als argument. Beschouw het volgende voorbeeld:

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

Dit voorbeeld identificeert een lambda voor het uitvoeren van een bericht naar de standaard uitvoerstroom. ()Identificeert van links naar rechts de formele parameterlijst van de lambda (er zijn geen parameters in het voorbeeld), ->geeft aan dat de uitdrukking een lambda is en System.out.println("Hello")de code is die moet worden uitgevoerd.

Lambdas vereenvoudigt het gebruik van functionele interfaces , dit zijn geannoteerde interfaces die elk precies één abstracte methode declareren (hoewel ze ook elke combinatie van standaard, statische en privé methoden kunnen declareren). De standaard klassenbibliotheek biedt bijvoorbeeld een java.lang.Runnableinterface met een enkele abstracte void run()methode. De verklaring van deze functionele interface verschijnt hieronder:

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

De klassenbibliotheek annoteert Runnablemet @FunctionalInterface, wat een instantie is van het java.lang.FunctionalInterfaceannotatietype. FunctionalInterfacewordt gebruikt om die interfaces te annoteren die in lambda-contexten moeten worden gebruikt.

Een lambda heeft geen expliciet interfacetype. In plaats daarvan gebruikt de compiler de omringende context om af te leiden welke functionele interface moet worden geïnstantieerd wanneer een lambda wordt gespecificeerd - de lambda is gebonden aan die interface. Stel dat ik het volgende codefragment heb gespecificeerd, dat de vorige lambda als argument doorgeeft aan de constructor van de java.lang.Threadklasse Thread(Runnable target):

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

De compiler stelt vast dat de lambda wordt doorgegeven Thread(Runnable r)omdat dit de enige constructor is die voldoet aan de lambda: het Runnableis een functionele interface, de lege formele parameterlijst van de lambda komt ()overeen met run()de lege parameterlijst van de lambda , en de return types ( void) komen ook overeen. De lambda is gebonden aan Runnable.

Listing 1 presenteert de broncode aan een kleine applicatie waarmee je met dit voorbeeld kunt spelen.

Listing 1. LambdaDemo.java (versie 1)

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

Compileer Listing 1 ( javac LambdaDemo.java) en start de applicatie ( java LambdaDemo). Let op de volgende output:

Hello

Lambdas kan de hoeveelheid broncode die u moet schrijven aanzienlijk vereenvoudigen en kan de broncode ook veel gemakkelijker te begrijpen maken. Zonder lambdas zou u bijvoorbeeld waarschijnlijk de meer uitgebreide code van Listing 2 specificeren, die is gebaseerd op een instantie van een anonieme klasse die implementeert Runnable.

Listing 2. LambdaDemo.java (versie 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(); } }

Start de applicatie na het compileren van deze broncode. U zult dezelfde output ontdekken als eerder getoond.

Lambdas en de Streams API

Naast het vereenvoudigen van de broncode, spelen lambda's een belangrijke rol in Java's functioneel georiënteerde Streams API. Ze beschrijven functionaliteitseenheden die worden doorgegeven aan verschillende API-methoden.

Java lambda's in de diepte

Om lambda's effectief te gebruiken, moet u de syntaxis van lambda-expressies begrijpen, samen met het begrip van een doeltype. Je moet ook begrijpen hoe lambda interactie met scopes, lokale variabelen, de thisen superzoekwoorden, en uitzonderingen. Ik zal al deze onderwerpen behandelen in de secties die volgen.

Hoe lambda's worden geïmplementeerd

Lambda's worden geïmplementeerd in termen van de invokedynamicinstructie van de virtuele Java-machine en de java.lang.invokeAPI. Bekijk de video Lambda: A Peek Under the Hood om meer te weten te komen over lambda-architectuur.

Lambda-syntaxis

Elke lambda voldoet aan de volgende syntaxis:

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

Het formal-parameter-listis een door komma's gescheiden lijst van formele parameters, die tijdens runtime moeten overeenkomen met de parameters van de enkele abstracte methode van een functionele interface. Als je hun typen weglaat, leidt de compiler deze typen af ​​uit de context waarin de lambda wordt gebruikt. Beschouw de volgende voorbeelden:

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

Lambdas en var

Beginnend met Java SE 11, kunt u een typenaam vervangen door var. U kunt bijvoorbeeld specificeren (var a, var b).

U moet haakjes opgeven voor meerdere of geen formele parameters. U kunt de haakjes echter weglaten (hoewel dit niet nodig is) wanneer u een enkele formele parameter opgeeft. (Dit is alleen van toepassing op de parameternaam - haakjes zijn vereist wanneer het type ook wordt opgegeven.) Bekijk de volgende aanvullende voorbeelden:

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

De formal-parameter-listwordt gevolgd door een ->teken, gevolgd door expression-or-statements-een uitdrukking of een blok met uitspraken (beide staan ​​bekend als het lichaam van de lambda). In tegenstelling tot op expressies gebaseerde body's, moeten op statements gebaseerde body's tussen accolades open ( {) en close ( }) worden geplaatst :

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

De op expressie gebaseerde lambda-body van het eerste voorbeeld hoeft niet tussen accolades te worden geplaatst. Het tweede voorbeeld converteert de op een expressie gebaseerde hoofdtekst naar een op een instructie gebaseerde hoofdtekst, waarin returnmoet worden gespecificeerd om de waarde van de uitdrukking te retourneren. Het laatste voorbeeld toont meerdere uitspraken en kan niet worden uitgedrukt zonder de accolades.

Lambda-lichamen en puntkomma's

Let op de afwezigheid of aanwezigheid van puntkomma's ( ;) in de vorige voorbeelden. In elk geval wordt de lambda-body niet beëindigd met een puntkomma omdat de lambda geen statement is. Binnen een op instructies gebaseerde lambda-body moet elke instructie echter worden beëindigd met een puntkomma.

Listing 3 presenteert een eenvoudige applicatie die de lambda-syntaxis demonstreert; merk op dat deze lijst voortbouwt op de vorige twee codevoorbeelden.

Listing 3. LambdaDemo.java (versie 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); } }

Lijst 3 introduceert eerst de BinaryCalculatoren UnaryCalculatorfunctionele interfaces waarvan de calculate()methoden berekeningen uitvoeren op respectievelijk twee invoerargumenten of één invoerargument. Deze lijst introduceert ook een LambdaDemoklasse waarvan de main()methode deze functionele interfaces demonstreert.

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