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".

Комментарии (1)


  1. sergey-gornostaev
    07.12.2024 08:37

    Статья кратко описывает основные концепции и преимущества использования лямбда-выражений в Java, а также их применение в сочетании с функциональными интерфейсами и новыми возможностями, такими как stream() и ссылки на методы. 

    Всего-то десять лет с релиза этих возможностей прошло!