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".
sergey-gornostaev
Всего-то десять лет с релиза этих возможностей прошло!