Начните работу с лямбда-выражениями в Java

До Java SE 8 анонимные классы обычно использовались для передачи функций методу. Эта практика запутывала исходный код, что усложняло его понимание. Java 8 устранила эту проблему, добавив лямбды. В этом руководстве сначала представлена ​​функция языка лямбда, а затем приводится более подробное введение в функциональное программирование с использованием лямбда-выражений и целевых типов. Вы также узнаете , как лямбды взаимодействуют с областями, локальными переменными thisи superключевыми словами и Java исключениями. 

Обратите внимание, что примеры кода в этом руководстве совместимы с JDK 12.

Открывать для себя типы

Я не буду представлять в этом руководстве какие-либо особенности языка, отличные от лямбда, о которых вы ранее не знали, но я продемонстрирую лямбда-выражения с помощью типов, которые я ранее не обсуждал в этой серии. Один из примеров - это java.lang.Mathкласс. Я представлю эти типы в будущих руководствах по Java 101. А пока я предлагаю прочитать документацию по API JDK 12, чтобы узнать о них больше.

загрузить Получить код Загрузите исходный код для примеров приложений из этого руководства. Создано Джеффом Фризеном для JavaWorld.

Лямбды: грунтовка

Лямбда - выражение (лямбда) описывает блок кода (анонимная функция) , которая может быть передана к конструкторам или методам для последующего выполнения. Конструктор или метод получает лямбда в качестве аргумента. Рассмотрим следующий пример:

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

В этом примере определяется лямбда для вывода сообщения в стандартный поток вывода. Слева направо ()определяет список формальных параметров лямбда (в примере нет параметров), ->указывает, что выражение является лямбда, и System.out.println("Hello")является кодом, который должен быть выполнен.

Лямбда-выражения упрощают использование функциональных интерфейсов , которые представляют собой аннотированные интерфейсы, каждый из которых объявляет ровно один абстрактный метод (хотя они также могут объявлять любую комбинацию стандартных, статических и частных методов). Например, стандартная библиотека классов предоставляет java.lang.Runnableинтерфейс с одним абстрактным void run()методом. Объявление этого функционального интерфейса приведено ниже:

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

Библиотека классов аннотирования Runnableс @FunctionalInterface, который является экземпляром java.lang.FunctionalInterfaceтипа аннотаций. FunctionalInterfaceиспользуется для аннотирования тех интерфейсов, которые будут использоваться в лямбда-контекстах.

Лямбда не имеет явного типа интерфейса. Вместо этого компилятор использует окружающий контекст, чтобы сделать вывод о том, какой функциональный интерфейс следует создать, когда указана лямбда - лямбда привязана к этому интерфейсу. Например, предположим, что я указал следующий фрагмент кода, который передает предыдущую лямбду в качестве аргумента конструктору java.lang.Threadкласса Thread(Runnable target):

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

Компилятор определяет, что лямбда передается, Thread(Runnable r)потому что это единственный конструктор, который удовлетворяет лямбда: Runnableэто функциональный интерфейс, пустой список формальных параметров лямбда ()совпадает run()с пустым списком параметров, и возвращаемые типы ( void) также согласны. Лямбда обязана Runnable.

В листинге 1 представлен исходный код небольшого приложения, которое позволяет вам поиграть с этим примером.

Листинг 1. LambdaDemo.java (версия 1)

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

Скомпилируйте листинг 1 ( javac LambdaDemo.java) и запустите приложение ( java LambdaDemo). Вы должны увидеть следующий результат:

Hello

Лямбда-выражения могут значительно упростить объем исходного кода, который вы должны написать, а также могут значительно упростить понимание исходного кода. Например, без лямбда-выражений вы, вероятно, укажете более подробный код в листинге 2, который основан на экземпляре анонимного класса, который реализует Runnable.

Листинг 2. LambdaDemo.java (версия 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(); } }

После компиляции этого исходного кода запустите приложение. Вы обнаружите тот же результат, что и ранее.

Лямбды и Streams API

Помимо упрощения исходного кода, лямбда-выражения играют важную роль в функционально ориентированном Java Streams API. Они описывают единицы функциональности, которые передаются различным методам API.

Подробно о лямбдах Java

Чтобы эффективно использовать лямбда-выражения, вы должны понимать синтаксис лямбда-выражений наряду с понятием целевого типа. Кроме того, необходимо понять , как лямбды взаимодействуют с областями, локальными переменными thisи superключевыми словами и исключениями. Я расскажу обо всех этих темах в следующих разделах.

Как реализованы лямбды

Лямбда-выражения реализованы в терминах invokedynamicинструкций виртуальной машины Java и java.lang.invokeAPI. Посмотрите видео «Лямбда: взгляд изнутри», чтобы узнать о лямбда-архитектуре.

Лямбда-синтаксис

Каждая лямбда соответствует следующему синтаксису:

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

Это formal-parameter-listсписок формальных параметров, разделенных запятыми, которые должны соответствовать параметрам единственного абстрактного метода функционального интерфейса во время выполнения. Если вы опустите их типы, компилятор выведет эти типы из контекста, в котором используется лямбда. Рассмотрим следующие примеры:

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

Лямбды и var

Начиная с Java SE 11, вы можете заменить имя типа на var. Например, вы можете указать (var a, var b).

Вы должны указать круглые скобки для нескольких формальных параметров или без них. Однако вы можете опустить круглые скобки (хотя это и не обязательно) при указании одного формального параметра. (Это относится только к имени параметра - круглые скобки требуются, когда также указывается тип.) Рассмотрим следующие дополнительные примеры:

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.

Листинг 4. LambdaDemo.java (версия 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); } }