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

от автора

1. Что такое лямбда?

Мы знаем, что для переменной в Java можно присвоить ей «значение», и затем вы сможете что-то с ней сделать.

Integer a = 1; String s = "Hello"; System.out.println(s + a);

Но что, если вы хотите присвоить переменной в Java «блок кода»?

Например, я хочу присвоить переменной codeBlock в Java следующий блок кода:

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

codeBlock = public void doSomething(String s) {         System.out.println(s); }

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

Таким образом, мы успешно присвоили «блок кода» переменной в элегантной форме. Этот блок кода, или «функция, присвоенная переменной», и есть лямбда-выражение.

Но возникает вопрос: какого типа должна быть переменная codeBlock?

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

Если это звучит немного запутанно, продолжим с примерами. Добавим тип для переменной codeBlock:

Интерфейс, у которого есть только одна функция для реализации, называется «функциональным интерфейсом».

Чтобы предотвратить добавление новых функций в этот интерфейс, что привело бы к необходимости реализации нескольких функций (и, как следствие, сделало бы его нефункциональным интерфейсом), можно добавить аннотацию @FunctionalInterface. Это гарантирует, что никто не сможет добавить в интерфейс новые функции.

Таким образом, мы получаем полноценное объявление лямбда-выражения.

2. Какова роль лямбда-выражений?

Самое очевидное преимущество лямбда-выражений — это значительное сокращение кода.

Сравним лямбда-выражения с традиционными способами реализации интерфейса в Java:

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

В некоторых случаях реализация интерфейса нужна только один раз. Традиционная Java 7 требует определить «интерфейс, загрязняющий окружение», чтобы реализовать InterfaceImpl. В Java 8 использование лямбда-выражений делает код чище.

Лямбда-выражения объединяют функциональные интерфейсы, forEach, stream(), ссылки на методы и другие новые возможности, чтобы сделать код более лаконичным!

Перейдём сразу к примеру.

Предположим, что определение класса Student и значение List<Student> уже заданы.

@Getter @AllArgsConstructor public static class Student {   private String name;   private Integer age; }  List<Student> students = Arrays.asList(   new Student("Bob", 18),   new Student("Ted", 17),   new Student("Zeka", 18));

Теперь требуется вывести имена всех студентов, которым 18 лет.

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

@FunctionalInterface interface AgeMatcher {   boolean match(Student student); }  @FunctionalInterface interface Executor {   boolean execute(Student student); }  public static void matchAndExecute(List<Student> students, AgeMatcher matcher, Executor executor) {   for (Student student : students) {     if (matcher.match(student)) {       executor.execute(student);     }   } }  public static void main(String[] args) {          List<Student> students = Arrays.asList(                 new Student("Bob", 18),                 new Student("Ted", 17),                 new Student("Zeka", 18));          matchAndExecute(students,                          s -> s.getAge() == 18,                          s -> System.out.println(s.getName())); }

Этот код уже довольно лаконичен, но можно ли его ещё больше упростить?

Конечно, в Java 8 есть пакет функциональных интерфейсов (java.util.function), в котором определено множество готовых функциональных интерфейсов, пригодных для использования.

Таким образом, нет необходимости определять два функциональных интерфейса AgeMatcher и Executor. Вместо них можно использовать готовые интерфейсы Predicate и Consumer из пакета функциональных интерфейсов Java 8, так как их определение фактически идентично AgeMatcher и Executor.

Шаг 1: Упростим — используем пакет функциональных интерфейсов:

public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {         for (Student student : students) {             if (predicate.test(student)) {                 consumer.accept(student);             }         }     }

Цикл for-each в методе matchAndExecute на самом деле довольно громоздкий. Вместо него можно использовать метод forEach() из интерфейса Iterable. Метод forEach() сам по себе принимает параметр типа Consumer<T>.

Упростим — заменим цикл for-each методом Iterable.forEach()

public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {         students.forEach(s -> {             if (predicate.test(s)) {                 consumer.accept(s);             }         });     }

Так как matchAndExecute фактически выполняет операции только над List, от него можно избавиться и вместо него использовать функцию stream(). В stream() есть несколько методов, которые принимают параметры, такие как Predicate<T> и Consumer<T> (пакет java.util.stream, доступный с Java SE 8). Как только вы поймёте вышеизложенное, использование stream() станет простым и не потребует дополнительных объяснений.

Шаг 3: Упростим — используем stream() вместо статических функций:

students.stream()         .filter(s -> s.getAge() == 18)         .forEach(s -> System.out.println(s.getName()));

По сравнению с изначальным способом записи лямбда-выражений, этот подход значительно лаконичнее. Однако, если требуется изменить код, чтобы выводить всю информацию о студенте, используя s -> System.out.println(s), можно ещё больше упростить его с помощью ссылки на метод. Так называемая ссылка на метод позволяет заменить лямбда-выражение вызовом метода уже существующего объекта или класса. Формат записи следующий:

Шаг 4: Упростим — используем ссылки на методы вместо лямбда-выражений в forEach:

students.stream() .filter(s -> s.getAge() == 18)         .map(Student::getName) .forEach(System.out::println);

Пожалуй, это самая лаконичная версия, которую я могу написать.

Тем не менее, в Java есть ещё много аспектов лямбда-выражений, которые стоит обсудить и изучить. Например, как использовать особенности лямбда-выражений для параллельной обработки и т.д.

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

В завершение всех начинающих тестировщиков-автоматизаторов приглашаем на открытый урок на тему «Тестирование API: Postman и SoapUI». Записаться на урок можно на странице курса «Java QA Engineer. Basic».


ссылка на оригинал статьи https://habr.com/ru/articles/864480/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *