JavaRush /Java блог /Random /Кофе-брейк #268. Полное руководство по лямбда-выражениям ...

Кофе-брейк #268. Полное руководство по лямбда-выражениям в Java

Статья из группы Random
Источник: Medium Изучив это руководство, вы научитесь использовать лямбда-выражения и поймете, какие преимущества они предоставляют Java-разработчику. Кофе-брейк #268. Полное руководство по лямбда-выражениям в Java - 1Лямбда-выражения, представленные в Java 8, ознаменовали собой значительную эволюцию языка программирования Java, поскольку они привнесли в него элемент функционального программирования. В языке Java лямбда-выражения используются для оптимизации определенных шаблонов программирования и упрощения работы над кодом, что позволяет делать его более кратким и читабельным. До Java 8 решение простых задач, таких как сортировка списка или создание нового потока, требовало написания анонимных внутренних классов, которые могли быть довольно многословными и загроможденными. Например, давайте рассмотрим подход к сортировке списка с использованием анонимного внутреннего класса. Как выглядел код до Java 8:

Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});
Этот способ написания кода, хотя и был функциональным, не способствовал читабельности и лаконичности. По мере усложнения Java-приложений среди разработчиков росла потребность в более оптимизированном подходе к решению таких рутинных задач.

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

Лямбда-выражение по сути является анонимной функцией; то есть функция без имени. Она не связана ни с каким классом, как метод, но ее можно передавать, как если бы она был объектом, и выполнять по требованию. Эта особенность делает лямбда-выражения ключевой особенностью функционального программирования. Структура лямбда-выражения проста и состоит из трех частей:
  • Параметры: Они перечислены в круглых скобках. Например, в (x, y), символы x и y являются параметрами.
  • Токен стрелки: Символ -> отделяет параметры от тела выражения.
  • Тело (Body): Содержит выражения и инструкции для выполнения. Оно может быть одним выражением или блоком кода.
Используя лямбда-выражение, предыдущий пример сортировки выглядит следующим образом. С лямбда-выражением после выхода Java 8:

Collections.sort(list, (s1, s2) -> s1.compareTo(s2));
Как видите, этот пример ясно демонстрирует краткость и ясность, привнесенные лямбда-выражениями.

Зачем нужны лямбда-выражения?

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

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

Хотя лямбда-выражения могут показаться похожими на анонимные внутренние классы, они принципиально отличаются. Анонимный внутренний класс может создавать экземпляр любого типа, будь то интерфейс, класс или абстрактный класс. С другой стороны, лямбда-выражения по сути являются экземплярами функциональных интерфейсов — интерфейсов с одним абстрактным методом. Это различие имеет решающее значение, поскольку оно лежит в основе функций функционального программирования, которые лямбда-выражения привносят в Java. Например, интерфейс Runnable, который используется для выполнения кода в отдельном потоке, является отличным примером функционального интерфейса. В мире до Java 8 использование интерфейса Runnable предполагало создание анонимного внутреннего класса. С помощью лямбда-выражений ту же функциональность можно достичь гораздо меньшим количеством кода. До Java 8:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Running in a new thread");
    }
}).start();
После появления лямбда-выраженией в Java 8:

new Thread(() -> System.out.println("Running in a new thread")).start();
Как видите, лямбда-выражения представляют собой важный шаг вперед в эволюции Java, позволяя разработчикам писать более гибкий, эффективный и читаемый код. Они прокладывают путь функциональному программированию на Java, которое представляет собой сдвиг парадигмы традиционных объектно-ориентированных корней Java.

Синтаксис и структура лямбда-выражений

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

Лямбда-выражения в Java предоставляют ясный и краткий способ реализации экземпляров интерфейсов с одним методом (функциональных интерфейсов). Понимание их синтаксиса имеет решающее значение для использования их возможностей в упрощении кода. Лямбда-выражение состоит из трех основных частей:
  • Параметры: В скобках указаны входные данные для лямбда-выражения. Тип параметров может быть явно объявлен или выведен компилятором.
  • Токен стрелки: Символ ->, также известный как токен стрелки, отделяет параметры от тела лямбда-выражения.
  • Тело: Тело лямбда-выражения содержит код, определяющий действие лямбда-выражения. Оно может быть отдельным выражением или блоком кода, заключенным в фигурные скобки {}.
Например, простое лямбда-выражение, которое складывает два целых числа, можно записать так:

(int a, int b) -> a + b
Если контекст позволяет, Java может определить тип параметров, что позволит вам написать то же лямбда-выражение, что и здесь:

(a, b) -> a + b

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

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

() -> System.out.println("Hello, world!")
Один параметр: Если имеется только один параметр и его тип определяется, круглые скобки можно опустить:

name -> System.out.println("Hello, " + name)
Несколько параметров: Для нескольких параметров обязательны круглые скобки:

(int a, int b) -> a * b
Блок кода: Если лямбда-выражение должно выполнять несколько операторов, используйте блок кода:

(String name) -> {
    System.out.println("Name: " + name);
    return name.length();
}

Возвращаемые значения и побочные эффекты

Лямбда-выражения могут возвращать значение. Тип возвращаемого значения определяется компилятором на основе контекста. Если тело лямбда-выражения содержит одно выражение, возвращается результат этого выражения. Для блока кода необходимо явно использовать оператор return. Также лямбда-выражения могут иметь побочные эффекты (например, изменение переменной-члена), если они обращаются к внешним переменным. Это известно как “захват переменных” (variable capture) и позволяет лямбда-выражениям взаимодействовать со своей средой, хотя и с определенными ограничениями для обеспечения безопасного и предсказуемого поведения.

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

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

Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2);
Синтаксис и структура лямбда-выражений в Java обеспечивают баланс между простотой и гибкостью. Они позволяют разработчикам выражать сложные функциональные возможности с помощью минимального кода, улучшая читаемость и удобство обслуживания. По мере дальнейшего изучения мы увидим, что эти выражения являются не просто синтаксическим сахаром, а мощными инструментами, открывающими новые парадигмы программирования на Java.

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

Функциональный интерфейс в Java — это интерфейс, содержащий ровно один абстрактный метод. Это ограничение имеет решающее значение, поскольку оно позволяет представлять экземпляры функциональных интерфейсов с помощью лямбда-выражений. Простота наличия единственного абстрактного метода означает, что тело лямбда-выражения неявно обеспечивает реализацию этого метода. В Java функциональные интерфейсы не являются новой концепцией. Они существовали еще в более ранних версиях Java, но в Java 8 они были формализованы с помощью аннотации @FunctionalInterface. Эта аннотация не является обязательной, но помогает четко указать назначение интерфейса и гарантирует, что интерфейс поддерживает контракт на наличие только одного абстрактного метода. Например:

@FunctionalInterface
public interface SimpleFunctionalInterface {
    void execute();
}

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

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

SimpleFunctionalInterface sfi = () -> System.out.println("Lambda implementation");
sfi.execute(); // Выводы: реализация Lambda
Этот код показывает, как лямбда-выражение используется для реализации метода execute SimpleFunctionalInterface.

Общие функциональные интерфейсы в Java

В Java 8 в пакете java.util.function появилось несколько встроенных функциональных интерфейсов, которые предназначены для покрытия большинства распространенных случаев использования лямбда-выражений. Некоторые из широко используемых функциональных интерфейсов включают в себя: Predicate<T>: Принимает аргумент типа T и возвращает логическое значение. Обычно используется для фильтрации данных.

Predicate<String> nonEmptyStringPredicate = s -> !s.isEmpty();
Function<T, R>: Принимает аргумент типа T и возвращает результат типа R. Часто используется для преобразования данных.

Function<String, Integer> stringLengthFunction = String::length;
Consumer<T>: Принимает аргумент типа T и не возвращает результат (void). Обычно используется для выполнения действий над объектами.

Consumer<String> printConsumer = System.out::println;
Supplier<T>: Представляет поставщика (supplier) результатов без входных аргументов.

Supplier<LocalDateTime> currentTimeSupplier = LocalDateTime::now;

В чем преимущество функциональных интерфейсов

Функциональные интерфейсы являются мощной концепцией в Java, поскольку они устраняют разрыв между функциональной и объектно-ориентированной парадигмами. Они позволяют писать более модульный, гибкий и легко тестируемый код. Например, методы могут быть разработаны так, чтобы принимать типы функциональных интерфейсов в качестве параметров, что делает их более универсальными и универсальными. Рассмотрим метод, который принимает Predicate<String>:

public static List<String> filterStrings(List<String> list, Predicate<String> predicate) {
    return list.stream()
               .filter(predicate)
               .collect(Collectors.toList());
}
Этот метод можно использовать с различными лямбда-выражениями или ссылками на методы, каждый из которых обеспечивает различную логику фильтрации. Функциональные интерфейсы и лямбда-выражения вместе привносят аспект функционального программирования в Java, язык, традиционно известный своими объектно-ориентированными функциями. Эта комбинация повышает выразительность Java и открывает мир возможностей для более краткого, читаемого и поддерживаемого кода. По мере того, как мы продолжаем изучать лямбда-выражения и их применение, становится очевидным, как эти концепции изменили то, как разработчики Java пишут код и думают о нем.

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

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

Совместное использование лямбда-выражений и потоков

Лямбда-выражения и Stream API вместе обеспечивают мощный и выразительный способ обработки коллекций в Java. До Java 8 перебор коллекций и применение таких операций, как фильтрация, сортировка или сопоставление, требовали многословного и часто повторяющегося кода. Stream API благодаря краткости лямбда-выражений упрощает эти операции. Например, рассмотрим задачу фильтрации списка строк на основе некоторых критериев и последующего преобразования каждой строки в ее форму в верхнем регистре. Как код выглядел до Java 8:

List<String> filteredList = new ArrayList<>();
for (String str : list) {
    if (str.startsWith("J")) {
        filteredList.add(str.toUpperCase());
    }
}
С Java 8 Stream API и лямбда-выражениями:

List<String> filteredList = list.stream()
                                .filter(str -> str.startsWith("J"))
                                .map(String::toUpperCase)
                                .collect(Collectors.toList());

Ключевые операции с потоками с помощью лямбда-выражений

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

stream.filter(element -> element.matches("condition"))
Map: Преобразует каждый элемент потока с помощью предоставленной функции.

stream.map(element -> element.someTransformation())
Sorted: Сортирует элементы потока на основе компаратора, который можно кратко реализовать с помощью лямбда-выражения.

stream.sorted((e1, e2) -> e1.compareTo(e2))
ForEach: обходит каждый элемент потока. Лямбда-выражение определяет действие, выполняемое над каждым элементом.

stream.forEach(element -> System.out.println(element))
Reduce: Выполняет сокращение элементов потока с помощью заданной функции для получения единого совокупного результата.

stream.reduce(0, (subtotal, element) -> subtotal + element.someValue())

Stream API и параллельная обработка

Одним из наиболее значительных преимуществ Stream API является его способность беспрепятственно распараллеливать операции. Просто изменив stream() на parallelStream(), операции над потоком можно выполнять параллельно, задействуя несколько ядер процессора. Лямбда-выражения облегчают эту задачу, поскольку позволяют кратко, но ясно определять операции, выполняемые с элементами потока. Интеграция лямбда-выражений с Stream API в Java изменила способ работы разработчиков с коллекциями и потоками данных. Эта комбинация позволяет писать более читаемый, краткий и удобный в сопровождении код. Он улучшает функциональный стиль программирования, делая операции с коллекциями более выразительными и эффективными, особенно с дополнительным преимуществом простого распараллеливания.

Заключение

Лямбда-выражения произвели революцию в программировании на Java с момента их появления в Java 8, открыв эру функционального программирования. Они упростили синтаксис, улучшили читаемость и сделали код более эффективным, особенно при обработке коллекций с помощью Stream API. Эта интеграция означает смену парадигмы, делая Java более адаптируемой и соответствующей современным тенденциям программирования. Лямбда-выражения, развивая подход функционального программирования, стали незаменимым инструментом для разработчиков Java, существенно повлияв на эволюцию языка. Советуем также изучить:
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ