Zacznij korzystać z wyrażeń lambda w Javie

Przed Java SE 8 anonimowe klasy były zwykle używane do przekazywania funkcjonalności do metody. Ta praktyka zaciemniła kod źródłowy, czyniąc go trudniejszym do zrozumienia. Java 8 wyeliminowała ten problem, wprowadzając lambdy. Ten samouczek najpierw wprowadza funkcję języka lambda, a następnie zapewnia bardziej szczegółowe wprowadzenie do programowania funkcjonalnego z wyrażeniami lambda i typami docelowymi. Dowiesz się także jak lambdas interakcję z zakresów, zmiennych lokalnych, thisoraz supersłów kluczowych i wyjątków Java. 

Zwróć uwagę, że przykłady kodu w tym samouczku są zgodne z JDK 12.

Odkrywanie typów dla siebie

W tym samouczku nie będę wprowadzał żadnych funkcji języka innego niż lambda, o których wcześniej nie słyszałeś, ale pokażę lambdy za pomocą typów, których wcześniej nie omawiałem w tej serii. Jednym z przykładów jest java.lang.Mathklasa. Te typy przedstawię w przyszłych samouczkach dotyczących języka Java 101. Na razie sugeruję przeczytanie dokumentacji API JDK 12, aby dowiedzieć się więcej o nich.

pobierz Pobierz kod Pobierz kod źródłowy na przykład aplikacje w tym samouczku. Stworzone przez Jeffa Friesena dla JavaWorld.

Lambdy: podkład

Ekspresji lambda (lambda) opisuje bloku kodu (anonimowe funkcyjny), który może być przekazywany do konstruktorów i metod do późniejszego wykonania. Konstruktor lub metoda otrzymuje lambdę jako argument. Rozważmy następujący przykład:

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

Ten przykład identyfikuje lambdę do wyprowadzania komunikatu do standardowego strumienia wyjściowego. Od lewej do prawej ()identyfikuje formalną listę parametrów lambda (w przykładzie nie ma parametrów), ->wskazuje, że wyrażenie jest wyrażeniem lambda i System.out.println("Hello")jest kodem do wykonania.

Lambdy upraszczają korzystanie z interfejsów funkcjonalnych , które są interfejsami z adnotacjami, z których każdy deklaruje dokładnie jedną metodę abstrakcyjną (chociaż mogą również deklarować dowolną kombinację metod domyślnych, statycznych i prywatnych). Na przykład standardowa biblioteka klas udostępnia java.lang.Runnableinterfejs z pojedynczą void run()metodą abstrakcyjną . Deklaracja tego funkcjonalnego interfejsu znajduje się poniżej:

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

Biblioteka klas zawiera adnotacje za Runnablepomocą @FunctionalInterface, która jest instancją java.lang.FunctionalInterfacetypu adnotacji. FunctionalInterfacesłuży do opisywania tych interfejsów, które mają być używane w kontekstach lambda.

Lambda nie ma jawnego typu interfejsu. Zamiast tego kompilator używa otaczającego kontekstu, aby wywnioskować, który interfejs funkcjonalny ma zostać utworzony, gdy określona jest lambda - lambda jest powiązana z tym interfejsem. Na przykład załóżmy, że podałem następujący fragment kodu, który przekazuje poprzednią lambdę jako argument do konstruktora java.lang.Threadklasy Thread(Runnable target):

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

Kompilator ustala, że ​​lambda jest przekazywana, Thread(Runnable r)ponieważ jest to jedyny konstruktor, który spełnia warunki lambda: Runnablejest interfejsem funkcjonalnym, pusta lista parametrów formalnych lambdy ()pasuje do run()pustej listy parametrów, a zwracane typy ( void) również się zgadzają. Lambda jest ograniczona Runnable.

Listing 1 przedstawia kod źródłowy małej aplikacji, która pozwala bawić się tym przykładem.

Listing 1. LambdaDemo.java (wersja 1)

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

Skompiluj Listing 1 ( javac LambdaDemo.java) i uruchom aplikację ( java LambdaDemo). Należy zwrócić uwagę na następujące dane wyjściowe:

Hello

Lambdy mogą znacznie uprościć ilość kodu źródłowego, który trzeba napisać, a także mogą znacznie ułatwić zrozumienie kodu źródłowego. Na przykład bez wyrażeń lambd prawdopodobnie określiłbyś bardziej szczegółowy kod z listy 2, który jest oparty na wystąpieniu implementowanej anonimowej klasy Runnable.

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

Po skompilowaniu tego kodu źródłowego uruchom aplikację. Odkryjesz te same dane wyjściowe, co poprzednio.

Lambdy i Streams API

Oprócz uproszczenia kodu źródłowego lambdy odgrywają ważną rolę w funkcjonalnie zorientowanym Java Streams API. Opisują jednostki funkcjonalności, które są przekazywane do różnych metod API.

Szczegółowe omówienie lambd języka Java

Aby efektywnie używać lambd, musisz rozumieć składnię wyrażeń lambda wraz z pojęciem typu docelowego. Należy również zrozumieć, jak lambdy interakcję z zakresów, zmiennych lokalnych, thisoraz supersłów kluczowych i wyjątków. Omówię wszystkie te tematy w kolejnych sekcjach.

Jak są implementowane lambdy

Lambdy są implementowane pod kątem instrukcji wirtualnej maszyny Java invokedynamici java.lang.invokeAPI. Obejrzyj wideo Lambda: A Peek Under the Hood, aby poznać architekturę lambda.

Składnia lambda

Każda lambda jest zgodna z następującą składnią:

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

Jest formal-parameter-listto lista oddzielonych przecinkami parametrów formalnych, które muszą pasować do parametrów pojedynczej metody abstrakcyjnej interfejsu funkcjonalnego w czasie wykonywania. Jeśli pominiesz ich typy, kompilator wywnioskuje te typy z kontekstu, w którym używana jest lambda. Rozważ następujące przykłady:

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

Lambdy i var

Począwszy od wersji Java SE 11, nazwę typu można zamienić na var. Na przykład możesz określić (var a, var b).

Musisz określić nawiasy dla wielu parametrów formalnych lub bez nich. Możesz jednak pominąć nawiasy (chociaż nie musisz) podczas określania pojedynczego parametru formalnego. (Dotyczy to tylko nazwy parametru - nawiasy są wymagane, jeśli typ jest również określony). Rozważ następujące dodatkowe przykłady:

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

Po znaku formal-parameter-listnastępuje ->token, po którym następuje expression-or-statements- wyrażenie lub blok instrukcji (obie są znane jako treść lambdy). W przeciwieństwie do treści opartych na wyrażeniach, treści oparte na instrukcjach muszą być umieszczane między znakami nawiasów open ( {) i close ( }):

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

Treść lambda oparta na wyrażeniach pierwszego przykładu nie musi być umieszczana między nawiasami klamrowymi. Drugi przykład konwertuje treść opartą na wyrażeniach na treść opartą na instrukcjach, w której returnnależy określić, aby zwracała wartość wyrażenia. Ostatni przykład ilustruje wiele instrukcji i nie można ich wyrazić bez nawiasów klamrowych.

Ciała lambda i średniki

Zwróć uwagę na brak lub obecność średników ( ;) w poprzednich przykładach. W każdym przypadku treść lambda nie jest zakończona średnikiem, ponieważ lambda nie jest instrukcją. Jednak w treści wyrażenia lambda opartego na instrukcji każda instrukcja musi być zakończona średnikiem.

Listing 3 przedstawia prostą aplikację, która demonstruje składnię lambda; Zwróć uwagę, że ta lista opiera się na dwóch poprzednich przykładach kodu.

Listing 3. LambdaDemo.java (wersja 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 najpierw przedstawia interfejsy funkcyjne BinaryCalculatori UnaryCalculator, których calculate()metody wykonują obliczenia odpowiednio na dwóch argumentach wejściowych lub na jednym argumencie wejściowym. Ta lista wprowadza również LambdaDemoklasę, której main()metoda demonstruje te funkcjonalne interfejsy.

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