Статьи

Учебник по лямбда-выражениям Java

В этой статье мы расскажем о всеобъемлющем руководстве по лямбда-выражениям на Java.

1. Лямбда-выражения Java-учебник — Введение

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

Как мы уже говорили выше, лямбда-выражения Java являются безымянными функциями, которые можно передавать как постоянные значения. Это означает, что они могут присутствовать в любом месте, где могло присутствовать любое другое постоянное значение, но обычно записываются в качестве параметра в какую-либо другую функцию. Чтобы рассмотреть канонический пример, мы можем передать функцию сравнения в общую функцию сортировки , и вместо того, чтобы пытаться определить целую процедуру (и вызвать лексический разрыв и загрязнение пространства имен), чтобы описать это сравнение, мы можем просто передать Лямбда-выражение, описывающее сравнение. Давайте посмотрим на некоторые свойства лямбда-выражения:

  • Аноним: его все еще можно назвать анонимным, потому что у него нет явного имени.
  • Кратко: как уже упоминалось в случае с анонимными классами, мы пишем намного меньше кода на Lambdas по сравнению с анонимными классами.
  • Функция: лямбда больше похожа на функцию, чем на метод. Это потому, что метод принадлежит классу, а лямбда — нет. Но, как и метод, лямбда принимает список параметров, имеет тело и может также генерировать исключения.
  • Может передаваться: лямбда может передаваться другим функциям, как обычный параметр.

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

2. Написание лямбда-выражений

В этом разделе мы увидим, как лямбда-выражения Java могут сократить количество строк кода, которые необходимо написать для выполнения некоторых простых операций. Например, мы сравним количество строк кода, чтобы создать функцию Comparator. Чтобы провести сравнение, мы POJO простой класс POJO класс Student который содержит идентификатор Student в качестве Long и имя в качестве параметра String :

Student.java

1
2
3
4
5
6
7
public class Student {
   
   private Long id;
   private String name;
 
   // standard setters and getters
}

Очень общая практика программирования — сравнивать даже объекты POJO мы определяем в наших приложениях. Если мы хотим сравнить два объекта класса Student , мы можем сделать Comparator следующим образом:

Компаратор с анонимным классом

1
2
3
4
5
6
Comparator<Student> byId = new Comparator<Student>() {
   @Override
   public int compare(Student s1, Student s2) {
       return s1.getId().compareTo(s2.getId());
   }
};

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

pom.xml

1
Comparator<Student> byId = (s1, s2) -> s1.getId().compareTo(s2.getId());

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

Краткая реализация Lambda

1
Comparator<Student> byId = Comparator.comparing(Student::getId);

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

Лямбда-выражения Java - Лямбда-выражения
Лямбда-выражение
  • Лямбда-выражение начинается со списка параметров, которые передаются в функцию Comparator в приведенном выше случае.
  • Символ стрелки отделяет параметры лямбда-выражения от тела лямбды
  • Тело четко сравнивает два объекта ученика с их id и это выражение определяет возвращаемое значение лямбда

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

3. Лямбда-выражения против анонимного класса

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

Для примера сравнения, давайте создадим класс и метод, который принимает Runnable в качестве входных данных:

Runnable класс

1
2
3
4
5
public class RunnableInstance {
    public static void doSomething(Runnable runnable){
        runnable.run();
    }
}

Когда мы создаем Runnable с использованием класса Anonymous, он выглядит следующим образом:

Runnable с анонимным классом

1
2
3
4
5
6
7
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.print("Anonymous class implementation.");
    }
};
doSomething(runnable);

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

Работает с лямбда

1
2
Runnable runnable = () -> System.out.print("Lambda Expression.");
doSomething(runnable);

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

Краткий лямбда

1
doSomething(() -> System.out.print("Lambda Expression."));

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

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

Параллельное программирование

1
collection.map { // my lambda }

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

Лямбда с параллельным потоком

1
2
3
List<String> names = students.stream()
        .map(s -> s.getName().toUpperCase())
        .collect(Collectors.toList());

Функция map может работать параллельно в многоядерной среде для одновременной обработки нескольких объектов без каких-либо действий. Для этого требуется только то, что операционная система, на которой работает эта программа, должна быть многоядерной. Как только это условие выполнено, мы можем быть уверены, что любая операция, которая может быть распараллелена в заданных выражениях, будет выполнена автоматически.

5. Коллекции и потоки

Платформа Collections является одним из наиболее часто используемых API-интерфейсов в Java. Коллекции позволяют нам собирать подобные объекты в структуру данных, которая может быть оптимизирована для определенной цели. Все примеры впереди требуют коллекций объектов, поэтому представьте, что у нас есть коллекция объектов типа Student, как мы определили ранее:

Студенческая коллекция

1
List students = getStudentObjectCollection();

Мы начнем с нового метода stream() который был добавлен в интерфейс Collection. Поскольку все коллекции «расширяют» коллекцию, все коллекции Java унаследовали этот метод:

Студенческий поток

1
2
List students = getStudentObjectCollection();
Stream stream = students.stream(); // a stream of student objects

Несмотря на то, что это выглядит так, интерфейс Stream не является еще одним обычным типом коллекции. Мы можем видеть Stream как абстракцию «потока данных», которая позволяет нам преобразовывать или манипулировать данными, которые он содержит. В отличие от других коллекций, которые мы изучали в Java, Stream не позволяет нам иметь прямой доступ к элементу, который он содержит. Хотя, если вы хотите получить доступ к элементам, мы всегда можем преобразовать поток в одну из коллекций в Java и выполнить нашу цель.

В демонстрационных целях мы посмотрим, как будет выглядеть наш код, если бы нам нужно было подсчитать, сколько объектов с нечетным идентификатором у нас в нашей коллекции students . Сначала посмотрим, как это можно сделать без использования потоков:

Подсчет нечетных идентификаторов

1
2
3
4
5
6
7
long count = 0;
List students = getStudentObjectCollection();
for (Student s : students) {
    if (s.getId() % 2 == 1) {
        count++;
    }
}

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

Мы можем написать точно такой же код, используя Stream в одной строке:

Использование Stream

1
2
List students = getStudentObjectCollection();
long count = students.stream().filter(student -> student.getId() % 2 == 1).count();

Разве это не выглядит аккуратнее и чище, чем предыдущий подход с циклом for ? Все началось с вызова метода stream() который преобразовал данную коллекцию в поток, а все остальные вызовы были объединены в цепочку, поскольку большинство методов в интерфейсе потока были разработаны с учетом паттерна построителя. Для тех, кто не привык к таким цепочкам методов, может быть проще визуализировать так:

Визуализация потока

1
2
3
4
List students = getStudentObjectCollection();
Stream stream = students.stream();
stream = stream.filter(student -> student.getId() % 2 == 1);
long count = stream.count();

Давайте сосредоточим наше внимание на двух методах потока, которые мы использовали, filter() и count() .

Метод filter() принимает условие, по которому мы хотим отфильтровать нашу коллекцию, и это условие представляется лямбда-выражением, которое принимает один параметр и возвращает логическое значение:

Лямбда-условие

1
student -> student.getId() % 2 == 1

Не случайно, функциональный интерфейс, используемый для представления этого выражения, параметр метода filter() , является интерфейсом Predicate. У него есть только один абстрактный метод, boolean test(T t) :

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

1
2
3
4
5
6
7
@FunctionalInterface
public interface Predicate {
 
    boolean test(T t);
 
    // non-abstract methods here
}

Параметризованный тип T представляет тип элемента нашего потока, то есть объектов Student. После фильтрации остается только вызвать метод count() . В этом нет ничего особенного, он просто подсчитывает, сколько объектов мы оставили в нашем потоке после фильтрации (у нас могло бы быть гораздо больше вещей, кроме простой фильтрации). Метод count() считается «терминальной операцией», и после его вызова поток считается «потребленным» и больше не может использоваться.

6. Недостатки лямбда-выражений

Хотя код с лямбда-выражениями выглядит очень лаконично, у лямбды есть и свои недостатки. Давайте рассмотрим некоторые из них здесь:

  • Не может обрабатывать проверенные исключения : любой код, который генерирует проверенные исключения, должен быть заключен в операторы try-catch. Но даже если мы это сделаем, не всегда понятно, что происходит с брошенным исключением.
  • Проблемы с производительностью : из-за того, что JIT не всегда может оптимизировать forEach() + lambda в той же степени, что и обычные циклы, Lambdas может влиять на производительность в очень незначительной степени.
  • Проблемы отладки . Понятно, что в Lambdas код не всегда настолько ясен, насколько он лаконичен. Это делает трассировку стека исключений, возникающих в коде, и читабельность немного затруднительной.

Хотя у Lambdas есть некоторые недостатки, они все равно становятся отличным компаньоном, когда вы пишете краткий код.

7. Заключение

Java Лямбда-выражения появляются (с разным синтаксисом) во всех LISP, Perl, Python и достаточно последних версиях C ++, Objective C, C # и Java 8, но особенно не в C, даже несмотря на то, что у них есть способ иметь дело с передачей функций ( или какое-то оправдание им) вокруг в качестве параметров. Они являются синтаксическим элементом с определенной семантикой, и эта семантика предъявляет к среде выполнения больше требований, чем требовалось для C.

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

Последнее обновление 17 февраля 2020 г.