Започнете с ламбда изрази в Java

Преди Java SE 8, анонимните класове обикновено се използваха за предаване на функционалност на метод. Тази практика замъглява изходния код, което го прави по-трудно за разбиране. Java 8 елиминира този проблем, като въведе ламбда. Този урок първо въвежда функцията за ламбда език, след това предоставя по-подробно въведение във функционалното програмиране с ламбда изрази заедно с целевите типове. Ще научите също как lambdas взаимодействат с обхват, местните променливи, thisи superключовите думи и Java изключения. 

Имайте предвид, че примерите за кодове в този урок са съвместими с JDK 12.

Откриване на типове за себе си

В този урок няма да въвеждам никакви не-ламбда езикови функции, за които преди не сте научавали, но ще демонстрирам ламбда чрез типове, които не съм обсъждал преди това в тази поредица. Един пример е java.lang.Mathкласът. Ще представя тези типове в бъдещи уроци за Java 101. Засега предлагам да прочетете документацията за JDK 12 API, за да научите повече за тях.

изтегляне Вземете кода Изтеглете изходния код, например приложения в този урок. Създадено от Jeff Friesen за JavaWorld.

Lambdas: Буквар

А ламбда експресия (ламбда) описва блок на код (анонимен функция), които могат да бъдат предадени на конструктори или методи за последващо изпълнение. Конструкторът или методът получава ламбда като аргумент. Обмислете следния пример:

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

Този пример идентифицира ламбда за извеждане на съобщение към стандартния изходен поток. Отляво надясно ()идентифицира списъка с официални параметри на ламбда (в примера няма параметри), ->посочва, че изразът е ламбда и System.out.println("Hello")е кодът, който трябва да бъде изпълнен.

Lambdas опростяват използването на функционални интерфейси , които са анотирани интерфейси, всеки от които декларира точно един абстрактен метод (въпреки че може да декларира всяка комбинация от стандартни, статични и частни методи). Например, стандартната библиотека на класове осигурява 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

Lambdas може значително да опрости количеството на изходния код, който трябва да напишете, и също така може да направи много по-лесен за разбиране изходния код. Например, без ламбда, вероятно бихте посочили по-подробния код от Листинг 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(); } }

След компилиране на този изходен код стартирайте приложението. Ще откриете същия изход, както е показано преди.

Lambdas и Streams API

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

Java ламбди в дълбочина

За да използвате ефективно ламбда, трябва да разберете синтаксиса на ламбда изразите заедно с понятието целеви тип. Можете също така трябва да се разбере как lambdas взаимодействат с обхват, местните променливи, thisи superключовите думи и изключения. Ще разгледам всички тези теми в следващите раздели.

Как се прилагат ламбдите

Lambdas са внедрени по отношение на инструкциите на Java виртуалната машина invokedynamicи java.lang.invokeAPI. Гледайте видеото Lambda: A Peek Under the Hood, за да научите повече за ламбда архитектурата.

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

Всяка ламбда отговаря на следния синтаксис:

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

Това formal-parameter-listе списък с формални параметри, разделен със запетая, който трябва да съвпада с параметрите на един абстрактен метод на функционален интерфейс по време на изпълнение. Ако пропуснете техните типове, компилаторът извежда тези типове от контекста, в който се използва ламбда. Обмислете следните примери:

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

Lambdas и 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

В formal-parameter-listе последвано от ->означението, което е последвано от expression-or-statements--an експресия или блок от изявления (или е известен като тялото на ламбда е). За разлика от телата, базирани на изрази, телата, базирани на изрази, трябва да се поставят между символи за скоби open ( {) и close ( }):

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

Базираното на изрази лямбда тяло на първия пример не трябва да се поставя между скоби. Вторият пример преобразува тялото, базирано на израза, в тяло, базирано на израз, в което returnтрябва да бъде посочено, за да върне стойността на израза. Последният пример демонстрира множество твърдения и не може да бъде изразен без скоби.

Ламбда тела и запетая

Обърнете внимание на отсъствието или наличието на точка и запетая ( ;) в предишните примери. Във всеки случай ламбда тялото не се прекратява с точка и запетая, тъй като ламбда не е изявление. Въпреки това, в рамките на текст на базата на изявление лямбда, всяко изявление трябва да бъде завършено с точка и запетая.

Листинг 3 представя просто приложение, което демонстрира ламбда синтаксис; имайте предвид, че този списък се основава на предишните два примера за код.

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

Листинг 3 първо въвежда BinaryCalculatorи UnaryCalculatorфункционалните интерфейси, чиито calculate()методи извършват изчисления съответно на два входни аргумента или на един входен аргумент. Този списък също така представя LambdaDemoклас, чийто main()метод демонстрира тези функционални интерфейси.

Функционалните интерфейси са показани в static double calculate(BinaryCalculator calc, double v1, double v2)и static double calculate(UnaryCalculator calc, double v)методи. Ламбдите предават код като данни на тези методи, които се получават като BinaryCalculatorили UnaryCalculatorекземпляри.

Компилирайте списък 3 и стартирайте приложението. Трябва да наблюдавате следния изход:

18 + 36.5 = 54.500000 89 / 2.9 = 30.689655 -89 = -89.000000 18 * 18 = 324.000000

Видове цели

Ламбда е свързан с неявен целеви тип , който идентифицира типа обект, към който е свързан ламбда. Целевият тип трябва да бъде функционален интерфейс, който се извежда от контекста, което ограничава ламбдатата да се появяват в следните контексти:

  • Декларация за променлива
  • Възлагане
  • Извлечение за връщане
  • Инициализатор на масив
  • Аргументи на метод или конструктор
  • Ламбда тяло
  • Тернарен условен израз
  • Изпълнение на ролите

Листинг 4 представя приложение, което демонстрира тези контексти на целевия тип.

Листинг 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); } }