JavaRush /Java блог /Архив info.javarush /Lambda-выражения на примерах
Автор
Aditi Nawghare
Инженер-программист в Siemens

Lambda-выражения на примерах

Статья из группы Архив info.javarush
Java изначально полностью объектно-ориентированный язык. За исключением примитивных типов, все в Java – это объекты. Даже массивы являются объектами. Экземпляры каждого класса – объекты. Не существует ни единой возможности определить отдельно (вне класса – прим. перев.) какую-нибудь функцию. И нет никакой возможности передать метод как аргумент или вернуть тело метода как результат другого метода. Все так. Но так было до Java 8. Lambda-выражения на примерах - 1Со времен старого доброго Swing, надо было писать анонимные классы, когда нужно было передать некую функциональность в какой-нибудь метод. Например, так выглядело добавление обработчика событий:

someObject.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                 
                //Event listener implementation goes here...
                 
            }
        });
Здесь мы хотим добавить некоторый код в слушатель событий от мыши. Мы определили анонимный класс MouseAdapter и сразу создали объект из него. Таким способом мы передали дополнительную функциональность в метод addMouseListener. Короче говоря, не так-то просто передать простой метод (функциональность) в Java через аргументы. Это ограничение вынудило разработчиков Java 8 добавить в спецификацию языка такую возможность как Lambda-выражения.

Зачем яве Lambda-выражения?

С самого начала, язык Java особо не развивался, если не считать такие вещи как аннотации (Annotations), дженерики (Generics) и пр. В первую очередь, Java всегда оставался объектно-ориентированным. После работы с функциональными языками, такими как JavaScript, можно понять насколько Java строго объектно-ориентирован и строго типизирован. Функции в Java не нужны. Сами по себе их нельзя встретить в мире Java. В функциональных языках программирования на первый план выходят функции. Они существуют сами по себе. Можно присваивать их переменным и передавать через аргументы другим функциям. JavaScript один из лучших примеров функциональных языков программирования. На просторах Интернета можно найти хорошие статьи, в которых детально описаны преимущества JavaScript как функционального языка. Функциональные языки имеют в своем арсенале такие мощные инструменты как замыкания (Closure), которые обеспечивают ряд преимуществ на традиционными способами написания приложений. Замыкание – это функция с привязанной к ней средой — таблицей, хранящей ссылки на все нелокальные переменные функции. В Java замыкания можно имитировать через Lambda-выражения. Безусловно между замыканиями и Lambda-выражениями есть отличия и не малые, но лямбда выражения являются хорошей альтернативой замыканиям. В своем саркастичном и забавном блоге, Стив Иег (Steve Yegge) описывает насколько мир Java строго завязан на имена существительные (сущности, объекты – прим. перев.). Если вы не читали его блог, рекомендую. Он забавно и интересно описывает точную причину того, почему в Java добавили Lambda-выражения. Lambda-выражения привносят в Java функциональное звено, которого так давно не хватало. Lambda-выражения вносят в язык функциональность на равне с объектами. Хотя это и не на 100% верно, можно видеть, что Lambda-выражения не являясь замыканиями предоставляют схожие возможности. В функциональном языке lambda-выражения – это функции; но в Java, lambda-выражения – представляются объектами, и должны быть связаны с конкретным объектным типом, который называется функциональный интерфейс. Далее мы рассмотри, что он из себя представляет. В статье Марио Фаско (Mario Fusco) “Зачем в Java нужны Lambda-выражения” (“Why we need Lambda Expression in Java”) подробно описано, зачем всем современным языкам нужны возможности замыканий.

Введение в Lambda-выражения

Lambda-выражения – это анонимные функции (может и не 100% верное определение для Java, но зато привносит некоторую ясность). Проще говоря, это метод без объявления, т.е. без модификаторов доступа, возвращающие значение и имя. Короче говоря, они позволяют написать метод и сразу же использовать его. Особенно полезно в случае однократного вызова метода, т.к. сокращает время на объявление и написание метода без необходимости создавать класс. Lambda-выражения в Java обычно имеют следующий синтаксис (аргументы) -> (тело). Например:

(арг1, арг2...) -> { тело }
 
(тип1 арг1, тип2 арг2...) -> { тело }
Далее идет несколько примеров настоящих Lambda-выражений:

(int a, int b) -> {  return a + b; }
 
() -> System.out.println("Hello World");
 
(String s) -> { System.out.println(s); }
 
() -> 42
 
() -> { return 3.1415 };

Структура Lambda-выражений

Давайте изучим структуру lambda-выражений:
  • Lambda-выражения могут иметь от 0 и более входных параметров.
  • Тип параметров можно указывать явно либо может быть получен из контекста. Например (int a) можно записать и так (a)
  • Параметры заключаются в круглые скобки и разделяются запятыми. Например (a, b) или (int a, int b) или (String a, int b, float c)
  • Если параметров нет, то нужно использовать пустые круглые скобки. Например () -> 42
  • Когда параметр один, если тип не указывается явно, скобки можно опустить. Пример: a -> return a*a
  • Тело Lambda-выражения может содержать от 0 и более выражений.
  • Если тело состоит из одного оператора, его можно не заключать в фигурные скобки, а возвращаемое значение можно указывать без ключевого слова return.
  • В противном случае фигурные скобки обязательны (блок кода), а в конце надо указывать возвращаемое значение с использованием ключевого слова return (в противном случае типом возвращаемого значения будет void).

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

В Java, маркерные интерфейсы (Marker interface) – это интерфейсы без объявления методов и полей. Другими словами маркерные интерфейсы – это пустые интерфейсы. Точно также, функциональные интерфейсы (Functional Interface) – это интерфейсы только с одним абстрактным методом, объявленным в нем. java.lang.Runnable – это пример функционального интерфейса. В нем объявлен только один метод void run(). Также есть интерфейс ActionListener – тоже функциональный. Раньше нам приходилось использовать анонимные классы для создания объектов, реализующих функциональный интерфейс. С Lambda-выражениями, все стало проще. Каждое lambda-выражение может быть неявно привязано к какому-нибудь функциональному интерфейсу. Например, можно создать ссылку на Runnable интерфейс, как показано в следующем примере:

Runnable r = () -> System.out.println("hello world");
Подобное преобразование всегда осуществляется неявно, когда мы не указываем функциональный интерфейс:

new Thread(
    () -> System.out.println("hello world")
).start();
В примере выше, компилятор автоматически создает lambda-выражение как реализацию Runnable интерфейса из конструктора класса Thread: public Thread(Runnable r) { }. Приведу несколько примеров lambda-выражений и соответствующих функциональных интерфейсов:

Consumer<Integer> c = (int x) -> { System.out.println(x) };
 
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
 
Predicate<String> p = (String s) -> { s == null };
Аннотация @FunctionalInterface, добавленная в Java 8 согласно Java Language Specification, проверяет является ли объявляемый интерфейс функциональным. Кроме того, в Java 8 включен ряд готовых функциональных интерфейсов для использования с Lambda-выражениями. @FunctionalInterface выдаст ошибку компиляции, если объявляемый интерфейс не будет функциональным. Далее приводится пример определения функционального интерфейса:

@FunctionalInterface
public interface WorkerInterface {
 
    public void doSomeWork();
 
}
Как следует из определения, функциональный интерфейс может иметь только один абстрактный метод. Если попытаться добавить еще один абстрактный метод, то вылезет ошибка компиляции. Пример:

@FunctionalInterface
public interface WorkerInterface {
 
    public void doSomeWork();
     
    public void doSomeMoreWork();
 
}
Error

Unexpected @FunctionalInterface annotation 
    @FunctionalInterface ^ WorkerInterface is not a functional interface multiple 
    non-overriding abstract methods found in interface WorkerInterface 1 error
После определения функционального интерфейса, мы можем его использовать и получать все преимущества Lambda-выражений. Пример:
// определении функционального интерфейса
@FunctionalInterface
public interface WorkerInterface {
 
    public void doSomeWork();
 
}

public class WorkerInterfaceTest {
 
    public static void execute(WorkerInterface worker) {
        worker.doSomeWork();
    }
 
    public static void main(String [] args) {
 
      // вызов метода doSomeWork через анонимный класс
      // (классический способ)
      execute(new WorkerInterface() {
            @Override
            public void doSomeWork() {
               System.out.println("Worker вызван через анонимный класс");
            }
        });
     
      // вызов метода doSomeWork через Lambda-выражения
      // (нововведение Java 8)
      execute( () -> System.out.println("Worker вызван через Lambda") );
    }
 
}
Вывод:

Worker вызван через анонимный класс
Worker вызван через Lambda
Здесь мы определили свой собственный функциональный интерфейс и воспользовались lambda-выражением. Метод execute() способен принимать lambda-выражения в качестве аргумента.

Примеры Lambda-выражений

Лучший способ вникнуть в Lambda-выражения – это рассмотреть несколько примеров: Поток Thread можно проинициализировать двумя способами:

// Старый способ:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from thread");
    }
}).start();

// Новый способ:
new Thread(
    () -> System.out.println("Hello from thread")
).start();
Управление событиями в Java 8 также можно осуществлять через Lambda-выражения. Далее представлены два способа добавления обработчика события ActionListener в компонент пользовательского интерфейса:

// Старый способ:
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Кнопка нажата. Старый способ!");
    }
});

// Новый способ:
button.addActionListener( (e) -> {
        System.out.println("Кнопка нажата. Lambda!");
});
Простой пример вывода всех элементов заданного массива. Заметьте, что есть более одного способа использования lambda-выражения. Ниже мы создаем lambda-выражение обычным способом, используя синтаксис стрелки, а также мы используем оператор двойного двоеточия (::), который в Java 8 конвертирует обычный метод в lambda-выражение:

// Старый способ:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
    System.out.println(n);
}

// Новый способ:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));

// Новый способ с использованием оператора двойного двоеточия ::
list.forEach(System.out::println);
В следующем примере мы используем функциональный интерфейс Predicate для создания теста и печати элементов, прошедших этот тест. Таким способом вы можете помещать логику в lambda-выражения и делать что-либо на ее основе.

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
 
public class Main {
   
    public static void main(String [] a)  {
 
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
 
        System.out.print("Выводит все числа: ");
        evaluate(list, (n)->true);
 
        System.out.print("Не выводит ни одного числа: ");
        evaluate(list, (n)->false);
 
        System.out.print("Вывод четных чисел: ");
        evaluate(list, (n)-> n%2 == 0 );
 
        System.out.print("Вывод нечетных чисел: ");
        evaluate(list, (n)-> n%2 == 1 );
 
        System.out.print("Вывод чисел больше 5: ");
        evaluate(list, (n)-> n > 5 );
 
    }
 
    public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
        for(Integer n: list)  {
            if(predicate.test(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }
 
}
Вывод:

Выводит все числа: 1 2 3 4 5 6 7 
Не выводит ни одного числа: 
Вывод четных чисел: 2 4 6 
Вывод нечетных чисел: 1 3 5 7 
Вывод чисел больше 5: 6 7
Поколдовав над Lambda-выражениями можно вывести квадрат каждого элемента списка. Заметьте, что мы используем метод stream(), чтобы преобразовать обычный список в поток. Java 8 предоставляет шикарный класс Stream (java.util.stream.Stream). Он содержит тонны полезных методов, с которыми можно использовать lambda-выражения. Мы передаем lambda-выражение x -> x*x в метод map(), который применяет его ко всем элементам в потоке. После чего мы используем forEach для печати всех элементов списка.

// Старый способ:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
    int x = n * n;
    System.out.println(x);
}

// Новый способ:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
Дан список, нужно вывести сумму квадратов всех элемента списка. Lambda-выражения позволяет достигнуть этого написанием всего одной строки кода. В этом примере применен метод свертки (редукции) reduce(). Мы используем метод map() для возведения в квадрат каждого элемента, а потом применяем метод reduce() для свертки всех элементов в одно число.

// Старый способ:
List<Integer> 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<Integer> 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);

Отличие Lambda-выражений от анонимных класов

Главное отличие состоит в использовании ключевого слова this. Для анонимных классов ключевое слово ‘this’ обозначает объект анонимного класса, в то время как в lambda-выражении ‘this’ обозначает объект класса, в котором lambda-выражение используется. Другое их отличие заключается в способе компиляции. Java компилирует lambda-выражения с преобразованием их в private-методы класса. При этом используется инструкция invokedynamic, появившаяся в Java 7 для динамической привязки метода. Тал Вайс (Tal Weiss) описал в своем блоге как Java компилирует lambda-выражения в байт-код

Заключение

Марк Рейнхолд (Mark Reinhold - Oracle’s Chief Architect), назвал Lambda-выражения самым значительным изменением в модели программирования, которое когда-либо происходило — даже более значительным, чем дженерики (generics). Должно быть он прав, т.к. они дают Java программистам возможности функциональных языков программирования, которых так давно все ждали. Наряду с такими новшествами как методы виртуального расширения (Virtual extension methods), Lambda-выражения позволяют писать очень качественный код. Я надеюсь, что это статья позволила вам взглянуть под капот Java 8. Удачи :)
Комментарии (67)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Дмитрий Уровень 40 Expert
8 июля 2024
В этом примере не раскрывается того, что по факту происходит внутри при работе, показан просто синтаксис как в метод execute передать лямбду и в двух слов написано, что "вот так делай будет норм" Вот пример из статьи public class WorkerInterfaceTest { public static void execute(WorkerInterface worker) { worker.doSomeWork(); } public static void main(String [] args) { // вызов метода doSomeWork через анонимный класс // (классический способ) execute(new WorkerInterface() { @Override public void doSomeWork() { System.out.println("Worker вызван через анонимный класс"); } }); // вызов метода doSomeWork через Lambda-выражения // (нововведение Java 8) execute( () -> System.out.println("Worker вызван через Lambda") ); } } в вызове метода execute, где в качестве аргумента передается анонимный класс все более-менее понятно, создается безымянный класс, который является наследником интерфейса WorkerInterface и в теле мы переопределяем единственный метод этого интерфейса doSomeWork(), тут какбы все ок. Но в нижнем примере где в качестве аргумента в метод execute передается лямбда execute( () -> System.out.println("Worker вызван через Lambda") ); , ваще не понятно, как эта лямбда внутри "под капотом" раскрывается?????? При определении метода execute четко написано, что он принимает параметр worker, который имеет тип WorkerInterface, но при вызове этого метода и передачей в него лямбды в примере и близко нигде нет объектов worker с подобным типом, так что все таки происходит внутри, когда в примере происходит такой вызов метода execute( () -> System.out.println("Worker вызван через Lambda") ); ?????
safe Уровень 35
13 июня 2024
после указания на отличия в способе компиляции, пришло таки понимание, я надеюсь, что это за лямбды.
Zhandos Уровень 41
27 декабря 2023
как я понял чтобы понять лямбда выражения сначала нужно понять что такое анонимный класс и для чего нужны интерфейсы и под лямбды лежит некии обьект который реализует метод функционального интерфейса? например n -> n % 2 == 0; это метод функционального интерфейса Predicate<T> boolean test(T t) и в лямбде как бы создаем анонимный класс который реализует этот метод правильно я понимаю?
Anonymous #3096996 Уровень 2
3 ноября 2023
Отличная статья, разобрался наконец-то) Долго откладывал лямбды, т к казались чем-то сложным
Denis Gritsay Уровень 41
28 октября 2023
это : // Старый способ: List<Integer> 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<Integer> 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
17 сентября 2023
Безусловно полезная статья, как и многие комментарии под лекциями на платформе JavaRush. Спасибо!
Олег Уровень 79 Expert
1 сентября 2023
Интересно!
Dmitry Vidonov Уровень 29 Expert
28 августа 2023
Хорошая статья, спасибо!
chess.rekrut Уровень 26
17 августа 2023
easy
Alexander Rozenberg Уровень 32
27 июля 2023
fine