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. Удачи :)
Комментарии (64)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Zhandos
Уровень 30
27 декабря 2023, 17:00
как я понял чтобы понять лямбда выражения сначала нужно понять что такое анонимный класс и для чего нужны интерфейсы и под лямбды лежит некии обьект который реализует метод функционального интерфейса? например n -> n % 2 == 0; это метод функционального интерфейса Predicate<T> boolean test(T t) и в лямбде как бы создаем анонимный класс который реализует этот метод правильно я понимаю?
prottobone
Уровень 28
Expert
6 февраля, 09:32
Да, вместо того чтобы вручную создавать некий класс, который реализует функциональный интерфейс, переопределять в нем абстрактный метод, потом создавать объект этого класса и вызывать на нем переопределенный метод... мы используем лямбду, а компилятор делает все вышеперечисленное за кулисами. Нам остается только указать как мы переопределим данный метод в лямбда выражении.
Anonymous #3096996
Уровень 2
3 ноября 2023, 14:51
Отличная статья, разобрался наконец-то) Долго откладывал лямбды, т к казались чем-то сложным
Denis Gritsay
Уровень 35
28 октября 2023, 16:08
это : // Старый способ: 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 век когда проблема с бумагой, а там где это реально полезно и нужно
Daria
Уровень 32
19 декабря 2023, 09:17
если немного упорядочить, то можно читать код, как текст, и не париться :)
list.stream()
     .map(x -> x * x)
     .reduce((x, y) -> x + y)
     .get();
Denis Gritsay
Уровень 35
19 декабря 2023, 09:33
мне лично не понятно на вскидку что делает это .reduce((x, y) -> x + y), понятно что выше мы перемножили, а потом?
Daria
Уровень 32
19 декабря 2023, 10:59
размышляя в таком ключе, можно сказать, что в целом язык непонятен и нечитабелен😂 нужно разбираться... например, сейчас я залезла в документацию, открыла stream api, нашла метод reduce и обнаружила, что
API Note:
Sum, min, max, average, and string concatenation are all special cases of reduction. Summing a stream of numbers can be expressed as:
     Integer sum = integers.reduce(0, (a, b) -> a+b);
or:
     Integer sum = integers.reduce(0, Integer::sum);
суммирование - частный случай уменьшения потока, тк на выходе получается один элемент(сумма), который получился из всех вошедших с потоком элементов. без этого знания, конечно, тяжелее, но профессиональнее :)
Denis Gritsay
Уровень 35
19 декабря 2023, 11:12
если вы посмотрите рейтинг решений задач на codewars, то обнаружите, что выше рейтинг имеют наиболее читабельные и простые, в том числе стримы. конечно, разобраться во всем можно, а потом писать код, который, хе хе, половине непонятен "потомучтонужнохорошоучить".
Daria
Уровень 32
19 декабря 2023, 11:16
у нас с Вами два разных мнения, и оба имеют место быть со своими минусами и плюсами :)
Denis Gritsay
Уровень 35
19 декабря 2023, 11:59
угу, программист пишет код для других программистов) где то слышал этот принцип
Vitaly Demchenko
Уровень 44
17 сентября 2023, 11:32
Безусловно полезная статья, как и многие комментарии под лекциями на платформе JavaRush. Спасибо!
Олег
Уровень 78
Expert
1 сентября 2023, 14:58
Интересно!
Dmitry Vidonov
Уровень 29
Expert
28 августа 2023, 17:43
Хорошая статья, спасибо!
chess.rekrut
Уровень 25
17 августа 2023, 07:53
easy
Alexander Rozenberg
Уровень 32
27 июля 2023, 19:16
fine
Ислам
Уровень 33
7 июня 2023, 09:42
Дает понимание того как работать с lambda
Ислам
Уровень 33
7 июня 2023, 09:42
Хорошая лекция