Kom igång med lambdauttryck i Java

Innan Java SE 8 användes vanligtvis anonyma klasser för att överföra funktionalitet till en metod. Denna övning fördunklade källkod, vilket gör det svårare att förstå. Java 8 eliminerade detta problem genom att introducera lambdas. Denna handledning introducerar först lambdaspråkfunktionen och ger sedan en mer detaljerad introduktion till funktionell programmering med lambdauttryck tillsammans med måltyper. Du får också lära dig hur lambdas samverkar med omfattningar, lokala variabler, de thisoch supersökord, och Java undantag. 

Observera att kodexempel i denna handledning är kompatibla med JDK 12.

Upptäck typer för dig själv

Jag kommer inte att presentera några funktioner som inte är lambdaspråk i denna handledning som du inte tidigare har lärt dig om, men jag kommer att visa lambdas via typer som jag inte tidigare har diskuterat i den här serien. Ett exempel är java.lang.Mathklassen. Jag kommer att presentera dessa typer i framtida Java 101-självstudier. För närvarande föreslår jag att du läser JDK 12 API-dokumentationen för att lära dig mer om dem.

ladda ner Skaffa koden Ladda ner källkoden till exempel applikationer i denna handledning. Skapad av Jeff Friesen för JavaWorld.

Lambdas: En grundfärg

Ett lambdauttryck (lambda) beskriver ett kodblock (en anonym funktion) som kan skickas till konstruktörer eller metoder för efterföljande körning. Konstruktören eller metoden tar emot lambda som ett argument. Tänk på följande exempel:

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

Detta exempel identifierar en lambda för att mata ut ett meddelande till standardutmatningsströmmen. Från vänster till höger ()identifierar lambdas formella parameterlista (det finns inga parametrar i exemplet), ->indikerar att uttrycket är en lambda och System.out.println("Hello")är koden som ska köras.

Lambdas förenklar användningen av funktionella gränssnitt , som är kommenterade gränssnitt som var och en förklarar exakt en abstrakt metod (även om de också kan deklarera vilken kombination som helst av standard-, statiska och privata metoder). Standardklassbiblioteket ger till exempel ett java.lang.Runnablegränssnitt med en enda abstrakt void run()metod. Det här funktionella gränssnittsdeklarationen visas nedan:

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

Klassbiblioteket kommenterar Runnablemed @FunctionalInterface, vilket är en instans av java.lang.FunctionalInterfaceannoteringstypen. FunctionalInterfaceanvänds för att kommentera de gränssnitt som ska användas i lambda-sammanhang.

En lambda har inte en uttrycklig gränssnittstyp. Istället använder kompilatorn det omgivande sammanhanget för att dra slutsatsen vilket funktionellt gränssnitt som ska startas när en lambda anges - lambda är bunden till det gränssnittet. Antag till exempel att jag angav följande kodfragment, som skickar föregående lambda som ett argument till java.lang.Threadklassens Thread(Runnable target)konstruktör:

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

Kompilatorn bestämmer att lambda skickas till Thread(Runnable r)eftersom det är den enda konstruktören som uppfyller lambda: Runnableär ett funktionellt gränssnitt, lambdas tomma formella parametervärde ()matchar den run()tomma parameterlistan och returtyperna ( void) är också överens. Lambda är bunden till Runnable.

Listning 1 presenterar källkoden för ett litet program som låter dig spela med detta exempel.

Listing 1. LambdaDemo.java (version 1)

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

Kompilera lista 1 ( javac LambdaDemo.java) och kör applikationen ( java LambdaDemo). Följ följande utdata:

Hello

Lambdas kan i hög grad förenkla mängden källkod som du måste skriva, och kan också göra källkoden mycket lättare att förstå. Till exempel utan lambdas skulle du antagligen ange Listing 2: s mer detaljerade kod, som är baserad på en instans av en anonym klass som implementeras 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(); } }

Efter att ha sammanställt den här källkoden, kör programmet. Du kommer att upptäcka samma utdata som tidigare visats.

Lambdas och Streams API

Förutom att förenkla källkoden spelar lambdas en viktig roll i Java: s funktionellt orienterade Streams API. De beskriver enheter av funktionalitet som skickas till olika API-metoder.

Java lambdas i djupet

För att använda lambdas effektivt måste du förstå syntaxen för lambdauttryck tillsammans med begreppet måltyp. Du måste också förstå hur lambdas samverkar med omfattningar, lokala variabler, de thisoch supersökord, och undantag. Jag kommer att täcka alla dessa ämnen i följande avsnitt.

Hur lambdas implementeras

Lambdas implementeras i enlighet med Java-virtuella maskinens invokedynamicinstruktion och java.lang.invokeAPI. Titta på videon Lambda: A Peek Under the Hood för att lära dig mer om lambda-arkitektur.

Lambda-syntax

Varje lambda överensstämmer med följande syntax:

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

Det formal-parameter-listär en kommaseparerad lista över formella parametrar som måste matcha parametrarna för ett funktionellt gränssnitts enkla abstrakta metod vid körning. Om du utelämnar deras typer, härleder kompilatorn dessa typer från det sammanhang där lambda används. Tänk på följande exempel:

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

Lambdas och var

Från och med Java SE 11 kan du ersätta ett typnamn med var. Du kan till exempel specificera (var a, var b).

Du måste ange parenteser för flera eller inga formella parametrar. Du kan dock utelämna parenteserna (även om du inte behöver) när du anger en enda formell parameter. (Detta gäller endast parameternamnet - parenteser krävs när typen också anges.) Tänk på följande ytterligare exempel:

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

Den formal-parameter-listföljs av en ->token, som följs av expression-or-statements- ett uttryck eller ett block av uttalanden (antingen kallas lambdas kropp). Till skillnad från uttrycksbaserade kroppar måste påståendebaserade kroppar placeras mellan öppna ( {) och nära ( }) brace-tecken:

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

Det första exemplets uttrycksbaserade lambdakropp behöver inte placeras mellan hängslen. Det andra exemplet konverterar den uttrycksbaserade kroppen till en uttalande-baserad kropp, i vilken returnmåste anges för att returnera uttryckets värde. Det sista exemplet visar flera påståenden och kan inte uttryckas utan hängslen.

Lambdakroppar och semikolon

Notera frånvaron eller närvaron av semikolon ( ;) i föregående exempel. I båda fallen avslutas inte lambdakroppen med ett semikolon eftersom lambda inte är ett uttalande. Inom en uttalandebaserad lambdakropp måste dock varje uttalande avslutas med ett semikolon.

Listning 3 presenterar en enkel applikation som visar lambdasyntax; notera att denna lista bygger på de två föregående kodexemplen.

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

Listning 3 introducerar först BinaryCalculatoroch UnaryCalculatorfunktionella gränssnitt vars calculate()metoder utför beräkningar på två inmatningsargument respektive på ett inmatningsargument. Denna lista introducerar också en LambdaDemoklass vars main()metod visar dessa funktionella gränssnitt.

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.

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