Что такое лямбда выражение в java
Перейти к содержимому

Что такое лямбда выражение в java

  • автор:

Лямбда-выражения в Java 8

Допустим, что мы пишем сервис управления подписками, и нам нужно выбирать различные списки электронных адресов. Выбираются они согласно условиям, которые могут изменяться и дополняться. Вот сервис:

public class SubscribeService < Listemails; public SubscribeService(List emails) < this.emails = emails; >public List getAllEmails() < return emails; >// другие методы, выдающие списки электронных адресов >

Первый метод простой — выдача всех адресов. Но другие методы сложнее, например:

//email из группы 1 public List getGroup1Emais() < Listlist = new ArrayList<>(); for (String email : emails) < if (email.contains(".group1")) < list.add(email); >> return list; > //email, начинающиеся с буквы "а" public List getAEmails() < Listlist = new ArrayList<>(); for (String email : emails) < if (email.startsWith("a")) < list.add(email); >> return list; > //email, начинающиеся с буквы "а" из группы1 public List getGroup1AEmails() < Listlist = new ArrayList<>(); for (String email : emails) < if (email.startsWith("a") && email.contains("group1")) < list.add(email); >> return list; >

Каждый раз надо придумывать новые имена, писать новые методы. Может это и не так сложно, но суть в том, что можно сделать проще с помощью лямбда-выражений.

Заметьте, что во всех вышеприведенных примерах мы используем какое-то условие,а значит можно написать функцию, которая возвращает boolean и ее передавать.

Именно здесь и можно применить лямбда-выражение — это блок кода, описывающий функцию интерфейса; функция эта передается как параметр в метод и вызывается при надобности (в нашем случае в тот момент, когда надо проверить условие см. функцию filter.test()).

В нашем случае код с лямбда-выражением будет выглядеть так, что вместо четырех методов появится один, принимающий условие как аргумент:

public List getEmails(Predicate filter) < Listlist = new ArrayList<>(); for (String email : emails) < if (filter.test(email)) < list.add(email); >> return list; >

А уж при вызове метода мы будем передавать лямбда-выражение:

service.getEmails((email) -> true);

Выше мы получили все электронные адреса.

Обратите внимание, что аргумент тут интерфейс Predicate из стандартной JDK 8.

Далее получим адреса, начинающиеся с буквы «a»:

service.getEmails((email)->email.startsWith("a"));

Аналог — анонимный класс

Лямбда выражения — это не что-то принципиально новое, появившееся в Java 8, а просто синтаксический сахар для анонимного класса. Раньше бы вместо вышеприведенного лямбда-выражения в параметр бы передавалась вот такая громоздкая конструкция:

service.getEmails(new Predicate() < @Override public boolean test(String email) < return email.startsWith("a"); >>);

Выглядит страшно, поэтому распространение получили такие вещи с Java 8 именно благодаря сахару.

Пора сказать пару слов о синтаксисе лямбда-выражения. По сути лямбда-выражение — это метод интерфейса, причем его единственный метод. Слева от стрелки «->» передается список аргументов метода, а справа — сам текст метода.

В лямбда-выражениях можно опускать слово return и точку с запятой, если правая часть состоит из одной строки. Также, не нужно указывать типы параметров, обычно компилатор их определяет из кода.

В аргументе getEmail() мы передаем интерфейс Predicate. Но как компилятор определяет, какой именно метод интерфейса передается?

Немного о функциональных интерфейсах

Predicate — это функциональный интерфейс, что означает, что он состоит только из одного метода (не считая статических и методов по умолчанию). Этот метод и вызывается для тестирования нашего условия. Мы можем передавать непосредственно содержимое этого метода, не указывания его имени. Компилятор и так догадается, что это за метод, поскольку в интерфейсе Predicate он всего один:

@FunctionalInterface public interface Predicate < boolean test(T t); //остальные методы статические либо default, они нам не мешают идентифицировать test >

Если бы из было два, то компилятор выдал бы ошибку. Лямбда-выражение можно передавать, только если оно реализует метод функционального интерфейса.

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

Таким образом, компилятор понимает, что email->email.startsWith(«a») — это реализация метода test() интерфейса Predicate.

Еще раз: поскольку метод единственный, и не надо уточнять его название, можно передать в параметр типа Predicate саму реализацию метода и аргумент. Idea сама предлагает заменить такой навороченный участок кода кратким лямбда-выражением.

При этом необязательно использовать интерфейс Predicate, можно создать свой интерфейс с другим названием, но такой же сигнатурой метода. Пример будет работать, просто Predicate уже есть в jdk, и плодить другие интерфейсы не рекомендуется для чистоты кода.

Допишем остальные методы

Допишем вызовы с лямбда-выражениями оставшихся двух методов.

Этот получает электронные адреса из группы1:

service.getEmails((email) -> email.contains("group1"));

А этот получает электронные адреса на букву «а» из группы1:

service.getEmails(email -> email.startsWith("a") && email.contains("group1"));

Подробнее о функциональных интерфейсах

Теперь закрепим идею функционального интерфейса. Как вы уже поняли из предыдущего примера, это интерфейс, который содержит ровно один абстрактный (в смысле не реализованный метод).

Вот примеры, угадайте является ли следующий интерфейс функциональным:

public interface Movable

Ответ нет, в интерфейсе Movable нет ни одного абстрактного метода, а надо один.

public interface Runnable extends Movable

Ответ да, в интерфейсе Runnable ровно один метод (из Movable не наследуется ни одного).

public interface FastRunnable extends Runnable < default void fastrun()< System.out.println("run fast"); >>

Интерфейс FastRunnable функциональный, поскольку он наследует один метод из Runnable, а default-метод fastrun() не учитывается.

public interface Swimming extends Movable< public static void ss()<>; >

Интерфейс Swimming не является функциональным, поскольку из Movable методы не наследуются, а метод ss() статический, а значит не учитывается.

Можно поупражняться в IDE в написании интерфейсов, проверяя ответ постановкой аннотации @FunctionalInterface вверху. Если интерфейс не фукциональный, компилятор выдаст ошибку.

Например, Runnable — функциональный, а значит можно поставить аннотацию @FunctionalInterface без проблем:

@FunctionalInterface public interface Runnable extends Movable

Подробнее о синтаксисе лямбда-выражения

Вернемся к примеру с электронными адресами. В нем среди прочих мы использовали такое лямбда-выражение:

email -> email.startsWith("a")

Выглядит оно не как полноценная функция, потому что в лямбда-выражених многие части опускаются. Вышеприведенный код эквивалентен следующему:

( String email ) ->

Слева от стрелки параметры, а справа тело метода.

Параметр метода здесь один, он имеет тип String. Возвращаемое значение тоже имеет тип String.

Это значит, что данное лямбда-выражение подойдет к любому функциональному интерфейсу, имеющему метод с одним параметром типа String и возвращающим значение такого же типа. Например, вместо встроенного в JDK 8 интерфейса Predicate мы могли бы определить и использовать любой собственный интерфейс:

public interface MyInterface1 < boolean test(String myparam1); >//Или такой: public interface MyInterface2 < boolean test(String myparam2); >//или такой: public interface MyInterface3

Название тут не важно, лишь бы было совпадение по количеству и типам аргументов.

Но обычно предпочтительнее использовать уже существующий интерфейс, а не писать свой, тем более почти все мыслимые интерфейсы с одним и двумя аргументами в JDK 8 уже есть и являются стандартными, как мы увидим ниже.

Когда можно опустить круглые скобки

Рассмотрим внимательнее левую часть двух вышеприведенных выражений:

Краткая форма

А это полная форма того же лямбда-выражения:

Во втором выражении присутствует тип аргумента и круглые скобки, а в первом нет.

Опустить круглые скобки слева можно в том и только том случае, если аргумент один, и его тип явно не задан (опущен).

Это означает, что например , если метод без аргументов, то круглые скобки должны присутствовать.

Вот примеры корректно написанных лямбда-выпажений:

() -> new Duck() d -> (Duck d) -> d.quack() (Animal a, Duck d) -> d.quack()

А вот примеры некорректных лямбда-выражений:

Duck d -> d.quack() // DOES NOT COMPILE a, d -> d.quack() // DOES NOT COMPILE Animal a, Duck d -> d.quack() // DOES NOT COMPILE
  1. В первом выражении задан тип, а значит нужны скобки.
  2. Во втором аргументов два, а не один, что означает, снова нужны скобки.
  3. В третьем снова два аргумента, а не один.

Фигурные скобки справа

Теперь сравним правую часть выражений, во втором выражении есть фигурные скобки <>.

Фигурные скобки дают возможность написать в правой части более одной строки кода. Но при этом нельзя опускать точку с запятой и оператор return, именно этому они во втором выражении и есть.

Как уже понятно, в первом выражении все это опущено потому, что одна строка кода позволяет все это опустить.

Приведем еще примеры корректный лямбда-выражений:

() -> true // 0 parameters a -> // 1 parameter (String a) -> a.startsWith("test") // 1 parameter (int x) -> <> // 1 parameter (int y) -> // 1 parameter (a, b) -> a.startsWith("test") // 2 parameters (String a, String b) -> a.startsWith("test") // 2 parameters

Найдите ошибки в лямбда-выражениях

А теперь некорректные, угадайте, что с ними не так:

a, b -> a.startsWith("test") // DOES NOT COMPILE c -> return 10; // DOES NOT COMPILE a -> < return a.startsWith("test") >// DOES NOT COMPILE
  1. В первом нужны круглые скобки слева, поскольку аргументов два.
  2. Во втором — фигурные скобки справа.
  3. В третьем пропущена точка с запятой (справа).

Привильный вариант такой:

(a, b) -> a.startsWith(«test») c -> < return 10; >a ->

А почему некорректны вот эти выражения, еще не упоминалось:

(int y, z) -> // DOES NOT COMPILE (String s, z) -> < return s.length()+z; >// DOES NOT COMPILE (a, Animal b, c) -> a.getName() // DOES NOT COMPILE

Ошибка в том, что если хотя бы один тип параметра слева указан, то остальные тоже должны быть указаны.

Чтобы сделать выражения корректными, надо либо убрать все типы (компилятор обычно их может определить из кода, не переживайте), либо, наоборот, указать все типы:

(y, z) -> (String s, int z) -> < return s.length()+z; >(a, b, c) -> a.getName()

Вот еще некорретное выражение:

(a, b) -> < int a = 0; return 5;>// DOES NOT COMPILE

Представьте его как обычный метод и поймете, что если есть аргумент а, то определять локальную переменную с тем же именем а нельзя.

Вот так будет правильно:

Ссылки на методы (Method references)

Но это еще не все, синтаксический сахар простирается дальше.

Рассмотрим случай, когда все, что мы делаем в лямбда-выражении — это вызываем другой метод. Например:

Consumer consumer= a->System.out.println(a);

Мы передаем аргумент a в метод интерфейса Consumer (стандартный интерфейс из jdk) и ничего не возвращаем (void). Этот аргумент передается дальше в метод println, который тоже ничего не возвращает. Сигнатуры методов одинаковые — и вышеприведенного метода интерфейса Consumer, и метода println:

System.out.println(argument);

На всякий случай вот код метода стандартного функционального интерфейса Consumer из jdk, в нем один параметр и возвращаемый тип void:

@FunctionalInterface public interface Consumer < void accept(T t); //default method >

Поскольку лямбда-выражение — это всего лишь код, описывающий входные параметры и тело метода, а входные параметры как лямбда выражения, так и метода, использованного внутри лямбда выражения одинаковые, нам достаточно указать один метод. Так что вместо такой записи:

Consumer consumer= a->System.out.println(a); (1)

можно использовать сокращенную запись — ссылку на метод println:

Consumer consumer=System.out::println; (2)

Записи (1) и (2) эквивалентны.

Дж. Блох рекомендует использовать method references везде, где это возможно.

Стандартные функциональные интерфейсы и примеры их использования в JDK 8

Если вы хотите передать в параметр функцию, то наверняка свой функциональный интерфейс писать не придется, так как уже есть куча стандартных интерфейсов с различными сигнатурами функций.

В пакете java.util.function их целых 43.

Но запомнить надо всего шесть, остальные интерфейсы производные от остальных. Например, мы выше использовали интерфейс Predicate, который возвращает boolean и принимает один аргумент. А есть BiPredicate — он такой же, но принимает два аргумента.

Вот эти шесть интерфейсов (в третьей колонке примеры с method reference, который мы рассматривали выше):

Интерфейс Сигнатура Пример из JDK
UnaryOperator T apply(T t) String::toLowerCase
BinaryOperator T apply(T t1, T t2) BigInteger::add
Predicate boolean test(T t) Collection::isEmpty
Function R apply(T t) Arrays::asList
Supplier T get() Instant::now
Consumer void accept(T t) System.out::println

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

А название из первого поясняет суть той или иной сигнатуры.

Например, BigInteger::add — бинарный оператор.

А вот (почти) все интерфейсы из java.util.function, которые поместились на скриншот:

java.utl.function

Проще зайти и посмотреть их сигнатуру, чем перечислять тут. Но они производятся по таким принципам:

    • Есть дополнительные три интерфейса BiFunction, BiPredicate, BiConsumer, принимающие два аргумента, а не один, в отличие от исходных Function, Predicate, Consumer. Пример:
    • Для всех интерфейсов, где это имеет смысл, есть производные с префиксом Double, Int, Long — они принимают конкретный примитивный тип, а не Generic, как исходные например:

    @FunctionalInterface public interface DoublePredicate < boolean test(double value); //. >

    public interface IntToDoubleFunction
    @FunctionalInterface public interface BooleanSupplier

    Замыкания в Java

    Еще один интересный (но не рекомендуемый) способ использования лямбда-выражений — это замыкания. Те, кто знает JavaScript, понимают о чем речь. Возьмем пример:

    public class Lambda < public static void main(String[] args) < Supplier generator = Lambda.generator(); System.out.println(generator.get()); System.out.println(generator.get()); System.out.println(generator.get()); >static Supplier generator() < Integer arr[] = ; return () -> ++arr[0]; > >

    К удивлению, результат в консоли будет таким:

    1 2 3

    На первый взгляд кажется магией, ведь локальные переменные в методах живут только во время метода, а потом забываются. А тут arr[] определенно запоминается.

    Но на самом деле arr[] находится не в методе, а в синтетическом final поле некоторого класса, который генерируется jvm и содержит нашу функцию () -> ++arr[0]; Так что все нормально.

    Итоги

    Пока итогов нет, статья будет дополняться. Исходный код некоторых примеров доступен на GitHub.

    Автор sysout Опубликовано 17.02.2019 13.01.2021 Рубрики Core Java

    Добавить комментарий Отменить ответ

    Прошу прощения: на комментарии временно не отвечаю.

    Lambda-выражения в Java

    Привет, Хабр! Представляю вашему вниманию перевод статьи «Java Lambda Expressions» автора www.programiz.com.

    Введение

    В этой статье, с помощью примеров, мы изучим lambda-выражения в Java, их использование с функциональными интерфейсами, параметризированными функциональными интерфейсами и Stream API.

    Лямбда выражения были добавлены в Java 8. Их основная цель – повысить читабельность и уменьшить количество кода.

    Но, прежде чем перейти к лямбдам, нам необходимо понимать функциональные интерфейсы.

    Что же такое функциональный интерфейс?

    Если интерфейс в Java содержит один и только один абстрактный метод, то он называется функциональным. Этот единственный метод определяет назначение интерфейса.

    Например, интерфейс Runnable из пакета java.lang является функциональным, потому, что он содержит только один метод run().

    Пример 1: объявление функционального интерфейса в java

    import java.lang.FunctionalInterface; @FunctionalInterface public interface MyInterface < // один абстрактный метод double getValue(); >

    В приведенном выше примере, интерфейс MyInterface имеет только один абстрактный метод getValue(). Значит, этот интерфейс — функциональный.

    Здесь мы использовали аннотацию FunctionalInterface, которая помогает понять компилятору, что интерфейс функциональный. Следовательно, не позволяет иметь более одного абстрактного метода. Тем не менее, мы можем её опустить.

    В Java 7, функциональные интерфейсы рассматривались как Single Abstract Methods (SAM). SAM обычно реализовывались с помощью анонимных классов.

    Пример 2: реализация SAM с помощью анонимного класса в java

    public class FunctionInterfaceTest < public static void main(String[] args) < // анонимный класс new Thread(new Runnable() < @Override public void run() < System.out.println("Я только что реализовал функциональный интерфейс Runnable.") >>).start(); > > 

    Результат выполнения:

    Я только что реализовал функциональный интерфейс Runnable. 

    В этом примере, мы принимаем анонимный класс для вызова метода. Это помогало писать программы с меньшим количеством строк кода в Java 7. Однако, синтаксис оставался достаточно сложным и громоздким.

    Java 8 расширила возможности SAM, сделав шаг вперед. Как мы знаем, функциональный интерфейс содержит только один метод, следовательно, нам не нужно указывать название метода при передаче его в качестве аргумента. Именно это и позволяет нам lambda-выражения.

    Введение в лямбда-выражения

    Лямбда-выражения, по сути, это анонимный класс или метод. Лямбда-выражение не выполняется само по себе. Вместо этого, оно используется для реализации метода, определенного в функциональном интерфейсе.

    Как записать лямбда-выражение в Java?

    В Java, лямбда-выражения имеют следующий синтаксис:

    (parameter list) -> lambda body 

    Здесь мы использовали новый оператор (->) — лямбда-оператор. Возможно, синтаксис кажется немного сложным. Давайте разберем пару примеров.

    Предположим, у нас есть такой метод:

    double getPiValue()

    Мы можем записать его, используя лямбда, как:

    () -> 3.1415

    Этот метод не имеет никаких параметров. Следовательно, левая часть выражения содержит пустые скобки. Правая сторона – тело лямбда-выражения, которое определяет его действие. В нашем случае, возвращается значение 3.1415.

    Типы лямбда-выражений

    В Java, тело лямбды может быть двух типов.

    1. Однострочные

    () -> System.out.println("Lambdas are great");

    2. Блочные (многострочные)

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

    Примечание: многострочные лямбда-выражения, всегда должны иметь оператор return, в отличии от однострочных.

    Пример 3: лямбда-выражение

    Давайте напишем Java программу, которая бы возвращала значение Pi, используя лямбда-выражение.

    Как говорилось ранее, лямбда-выражение не выполняется само собой. Скорее, оно формирует реализацию абстрактного метода, объявленного в функциональном интерфейсе.

    И так, для начала, нам необходимо описать функциональный интерфейс.

    import java.lang.FunctionalInterface; // функциональный интерфейс @FunctionalInterface interface MyInterface < // абстрактный метод double getPiValue(); >public class Main < public static void main( String[] args ) < // объявление ссылки на MyInterface MyInterface ref; // лямбда-выражение ref = () ->3.1415; System.out.println("Value of Pi java">Value of Pi = 3.1415 
    • Мы создали функциональный интерфейс MyInterface, который содержит один абстрактный метод getPiValue().
    • Внутри класса Main, мы объявили ссылку на MyInterface. Обратите внимание, что мы можем объявить ссылку на интерфейс, но не можем создать его объект.

    // приведет к ошибке MyInterface ref = new myInterface(); // это верно MyInterface ref; 
    ref = () -> 3.1415;
    System.out.println("Value of Pi java">(n) -> (n % 2) == 0

    В этом примере, переменная n внутри скобок является параметром, переданном в лямбда-выражение. Тело лямбды принимает параметр и проверяет его на четность.

    Пример 4: использование лямбда-выражения с параметрами

    @FunctionalInterface interface MyInterface < // абстрактный метод String reverse(String n); >public class Main < public static void main( String[] args ) < // объявление ссылки на MyInterface // присвоение лямбда-выражения ссылке MyInterface ref = (str) ->< String result = ""; for (int i = str.length()-1; i >= 0 ; i--) result += str.charAt(i); return result; >; // вызов метода из интерфейса System.out.println("Lambda reversed = " + ref.reverse("Lambda")); > > 

    Результат выполнения:

    Lambda reversed = adbmaL

    Параметризированный функциональный интерфейс

    До этого момента, мы использовали функциональные интерфейсы, которые принимали только один тип значения. Например:

    @FunctionalInterface interface MyInterface

    Вышеупомянутый функциональный интерфейс принимает только String и возвращает String. Однако, мы можем сделать наш интерфейс универсальным, чтобы использовать с любым типом данных.

    Пример 5: параметризированный интерфейс и лямбда-выражения

    // Параметризированный интерфейс @FunctionalInterface interface GenericInterface  < // параметризированный метод T func(T t); >public class Main < public static void main( String[] args ) < // Объявление ссылки на параметризированный интерфейс // который принимает String // и присвоение ей лямбды GenericInterfacereverse = (str) -> < String result = ""; for (int i = str.length()-1; i >= 0 ; i--) result += str.charAt(i); return result; >; System.out.println("Lambda reversed = " + reverse.func("Lambda")); // Объявление ссылки на параметризированный интерфейс // который принимает Integer // и присвоение ей лямбды GenericInterface factorial = (n) -> < int result = 1; for (int i = 1; i ; System.out.println("factorial of 5 java">Lambda reversed = adbmaL factorial of 5 = 120 

    В этом примере, мы создали параметризированный функциональный интерфейс GenericInterface, который содержит параметризированный метод func().

    Затем, внутри класса Main:

    • GenericInterface reverse – создает ссылку на интерфейс, который работает со String.
    • GenericInterface factorial — создает ссылку на интерфейс, который работает с Integer.

    Лямбда-выражения и Stream API

    В JDK8 добавлен новый пакет java.util.stream, который позволяет java-разработчикам выполнять такие операции, как поиск, фильтрация, сопоставление, объединение или манипулирование коллекциями, к примеру Lists.

    Например, у нас есть поток данных (в нашем случае список строк), где каждая строка содержит название страны и ее город. Теперь мы можем обработать этот поток данных и выбрать только города Непала.

    Для этого мы можем использовать комбинацию Stream API и лямбда-выражений.

    Пример 6: использование лямбд в Stream API

    import java.util.ArrayList; import java.util.List; public class StreamMain < // объявление списка static Listplaces = new ArrayList<>(); // заполнение данными public static List getPlaces() < // добавление страны и города places.add("Nepal, Kathmandu"); places.add("Nepal, Pokhara"); places.add("India, Delhi"); places.add("USA, New York"); places.add("Africa, Nigeria"); return places; >public static void main( String[] args ) < ListmyPlaces = getPlaces(); System.out.println("Places from Nepal:"); // Фильтрация городов myPlaces.stream() .filter((p) -> p.startsWith("Nepal")) .map((p) -> p.toUpperCase()) .sorted() .forEach((p) -> System.out.println(p)); > > 

    Результат выполнения:

    Places from Nepal: NEPAL, KATHMANDU NEPAL, POKHARA 

    В приведенном выше примере обратите внимание на это выражение:

    myPlaces.stream() .filter((p) -> p.startsWith("Nepal")) .map((p) -> p.toUpperCase()) .sorted() .forEach((p) -> System.out.println(p)); 

    Здесь мы используем такие методы, как filter(), map(), forEach() из Stream API, которые могут принимать лямбды в качестве параметра.

    Также, мы можем описать собственные выражения на основе синтаксиса, описанного выше. Это позволит нам уменьшить количество строк кода.

    • java 8
    • lambda expressions
    • stream api

    Функциональные интерфейсы и лямбда-выражения в Java

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

    Передача простых параметров в виде примитивов или объектов труда обычно не составляет. Но порой в метод требуется передавать не просто переменную, а исполняемый код.

    Например, нам нужен метод, который работает с элементами массива, причём только с теми, что соответствуют некоторому условию. А само условие во время написания метода мы можем не знать (или оно будет меняться).

    Как поступить? Передавать реализующий условие код с помощью параметра метода! Да, в Java начиная с восьмой версии можно подобное делать. И сейчас вы узнаете как.

    Пример

    Напишем методы, возвращающие сумму и произведение двух чисел:

    private int sum(int a, int b) < return a+b; > private int mult(int a, int b) < return a*b; >

    А теперь объединим их в один — processTwoNumbers. Он будет принимать два параметра-числа и код, который их обрабатывает.

    private int processTwoNumbers (int a, int b, [сюда передаётся код])

    Для выполнения метода sum третий параметр примет в качестве аргумента действие a+b, а для выполнения метода multa*b.

    Обратите внимание, что третьим аргументом может быть передан не любой код, а только тот, который принимает на вход два параметра заданного типа (у нас int) и возвращает переменную нужного типа (int).

    Значит, надо как-то сообщить об этом компилятору — запретить будущим разработчикам передавать неподходящий код (вроде a+b+c).

    Поможет в этом сигнатура метода. Она станет третьим параметром в нашем методе processTwoNumbers:

    private int processTwoNumbers (int a, int b, [сигнатура метода])

    Но как записать третий параметр, чтобы сигнатура самого метода processTwoNumbers не разрослась до нечитабельности? Этот вопрос разработчики Java решили изящно. Они придумали функциональные интерфейсы.

    Что такое функциональный интерфейс

    Функциональный интерфейс — это интерфейс, который содержит ровно один абстрактный метод, то есть описание метода без тела. Статические методы и методы по умолчанию при этом не в счёт, их в функциональном интерфейсе может быть сколько угодно.

    Когда параметром метода является функциональный интерфейс, при вызове этого метода одним из аргументов должен быть блок кода.

    Передаваемый блок кода должен удовлетворять следующему условию: его сигнатура должна совпадать с сигнатурой единственного абстрактного метода функционального интерфейса.

    Звучит непросто, поясним на примере:

    @FunctionalInterface public interface ToIntBiFunctionT, U> < /** * Applies this function to the given arguments. * * @param t the first function argument * @param u the second function argument * @return the function result */ int applyAsInt(T t, U u); >

    Важно. В Java есть несколько готовых функциональных интерфейсов с разным числом и типами входных-выходных параметров. (Как раз из таких ToIntBiFunction выше.) А если мы создаём новый функциональный интерфейс, то важно не забыть аннотацию @FunctionalInterface. Увидев её, компилятор проверит, что интерфейс и правда является функциональным.

    Функциональный интерфейс ToIntBiFunction подходит к тому примеру, с которого мы начинали. Это значит, что мы можем передать в него аргументом код, который:

    • принимает на вход два параметра (T t, U u). T и U указывают на то, что аргументы могут быть разных типов. Например, Long и String. Для нас это даже избыточно, у нас они одного типа — int;
    • возвращает значение типа int.

    Вот что получится:

    public void processTwoNumbers(int a, int b, ToIntBiFunctionInteger, Integer> function)< //code >

    Кусочек ToIntBiFunction говорит: передавай сюда метод с такой же сигнатурой, как у метода внутри меня.

    Чтобы внутри метода processTwoNumbers выполнить переданный код, нужно вызвать метод из функционального интерфейса:

    public void processTwoNumbers(int a, int b, ToIntBiFunctionInteger, Integer> function)< function.applyAsInt(a, b); >

    Вот мы и добрались до лямбда-выражений.

    Что такое лямбда-выражения

    Это компактный синтаксис, заимствованный из λ-исчисления, для передачи кода в качестве параметра в другой код.

    По сути, это анонимный (без имени) класс или метод. Так как всё в Java (за исключением примитивных типов) — это объекты, лямбды тоже должны быть связаны с конкретным объектным типом. Как вы догадались, он называется функциональным интерфейсом.

    То есть лямбда-выражение не выполняется само по себе, а нужно для реализации метода, который определён в функциональном интерфейсе.

    Не будь лямбд, вызывать метод processTwoNumbers каждый раз приходилось бы так:

    ToIntBiFunctionInteger, Integer> biFunction = new ToIntBiFunction<>() < @Override public int applyAsInt(Integer a, Integer b) < return a + b; > >; processTwoNumbers(1, 2, biFunction);

    Примечание. biFunction в примере создана с использованием анонимных классов. Без этого нам пришлось бы создавать класс, реализующий интерфейс ToIntBiFunction, и объявлять в этом классе метод applyAsInt. А с анонимным классом мы всё это сделали на лету.

    В примере выше всё, кроме одной строчки, избыточно. За содержательную часть (логику работы) отвечает только одно выражение return a + b, а всё остальное — техническая шелуха. И её пришлось написать многовато, даже чтобы просто передать методу код сложения двух чисел.

    Здесь и вступают в игру лямбды. С ними можно сократить создание biFunction всего до десяти символов!

    Лямбда строится так: (параметры) -> (код метода)

    А наша лямбда будет такой:

    ToIntBiFunction biFunction = (a, b) -> a + b;

    И всё! Этот блок из 10 символов можно передавать как аргумент методу, ожидающему функциональный интерфейс в качестве параметра. Причём чаще всего обходятся без промежуточной переменной — передают напрямую лямбду:

    processTwoNumbers(1, 2, (a, b) -> a+b);

    Компилятор проверит, что лямбда подходит функциональному интерфейсу — принимает нужное число параметров нужного типа. Напомню, в нашем примере задействован функциональный интерфейс ToIntBiFunction. Сигнатура его единственного абстрактного метода содержит два параметра (Integer a, Integer b).

    Например, такой вот вызов метода не скомпилируется, потому что передан всего один параметр:

    processTwoNumbers(5, 6, (a) -> 5 * a);

    Лямбды записывают по-разному. Мы рассмотрели только один вариант.

    Где применяют лямбды?

    Много где. Довольно частый случай — обход элементов в цикле:

    List integers = Arrays.asList(1, 2, 3, 4, 5); integers.forEach(item -> System.out.println(item));

    Ещё лямбды работают в компараторах при сортировке. Допустим, нужно отсортировать коллекцию по последней букве каждого слова:

    List colors = Arrays.asList("Black", "White", "Red"); Collections.sort(colors, (o1, o2) -> < String o1LastLetter = o1.substring(o1.length() - 1); String o2LastLetter = o2.substring(o2.length() - 1); return o1LastLetter.compareTo(o2LastLetter); >);

    Редко обходятся без лямбд при работе с коллекциями вместе со Stream API. В следующем примере фильтруем стрим по значению (filter), меняем каждый элемент (map) и собираем в список (collect):

    final double currencyUsdRub = 80; List pricesRub = Arrays.asList(25d, 50d , 60d, 12d, 45d, 89d); List pricesUsdGreater50Rub = pricesRub.stream() .filter(d -> d > 50) // используем функциональный интерфейс Predicate .map(d -> d / currencyUsdRub) // используем функциональный интерфейс Function .collect(Collectors.toList()); 

    Подытожим

    Функциональные интерфейсы в Java 8 избавили разработчиков от чудовищно громоздкого синтаксиса с анонимными классами (когда требовалось передавать некую функциональность в метод) и позволили использовать компактные лямбда-выражения и ссылки на методы.

    До восьмой версии Java разработчики обходились без лямбда-выражений. Лямбды стали для них очередным синтаксическим сахаром .

    Сперва синтаксическим сахаром были функциональные интерфейсы, они позволили оперировать блоком кода, который выполняется когда нужно, но реализации были слишком громоздкими. А с лямбдами функциональные интерфейсы стали записываться короче. Так что лямбды — не просто синтаксический сахар, а синтаксический сахар синтаксического сахара.

    Теперь на Java можно писать программы в стиле функциональных языков программирования (это когда программа записывается как последовательное применение функций к некоторым значениям и другим функциям, а не как сложная структура из циклов, условных операторов и перекладывания значений туда-сюда). Удивительно, как легко превратить массивные структуры кода в изящные цепочки вызовов, и всё это благодаря лямбдам и функциональным интерфейсам.

    Официальная документация по лямбдам здесь.

    Компаратор (от англ. compare, «сравнивать») — функция, которая принимает на вход два элемента, сравнивает их и отвечает: больше/меньше/равно.

    От англ. syntactic sugar — это избыточные синтаксические возможности языка программирования, придуманные только для удобства.

    Что такое лямбда выражение в java

    Zhandos Уровень 25

    27 декабря 2023

    как я понял чтобы понять лямбда выражения сначала нужно понять что такое анонимный класс и для чего нужны интерфейсы и под лямбды лежит некии обьект который реализует метод функционального интерфейса? например n -> n % 2 == 0; это метод функционального интерфейса Predicate boolean test(T t) и в лямбде как бы создаем анонимный класс который реализует этот метод правильно я понимаю?

    3 ноября 2023

    Отличная статья, разобрался наконец-то) Долго откладывал лямбды, т к казались чем-то сложным

    Denis Gritsay Уровень 32

    28 октября 2023

    это : // Старый способ: List list = Arrays.asList(1,2,3,4,5,6,7); int sum = 0; for(Integer n : list) < int x = n * n; sum = sum + x; >System.out.println(sum); намного понятнее и легче в восприятию чем это List list = Arrays.asList(1,2,3,4,5,6,7); int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get(); System.out.println(sum); Так что лямбду нужно я думаю использовать не там где пара лишних строк кода, ведь сейчас не 17 век когда проблема с бумагой, а там где это реально полезно и нужно

    Vitaly Demchenko Уровень 44

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *