GrabDuck

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

:

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

Со времен старого доброго 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-выражениями есть отличия и не малые, но 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. Удачи :)