Lambda-выражения
Узнаём, что такое лямбда-выражения и как они делают код компактнее. Разбираем функциональные интерфейсы, стандартные функции и method references
Проблема: слишком многословный код
До знакомства с лямбдами давайте посмотрим на типичную проблему: создание анонимного класса для простой операции. Представьте, что вы хотите отсортировать список имён по длине:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Б");
names.add("Виктор");
names.add("Дима");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
System.out.println(names);
}
}
Этот код работает, но выглядит громоздко: анонимный класс, перегруженный метод, повторение типов. Весь компаратор — это по сути одна строка: Integer.compare(a.length(), b.length()).
Аналогия: Представьте, что вы хотите попросить друга купить хлеб. Вы можете написать ему письмо на трёх страницах: «Уважаемый друг, я хотел бы вас попросить, если у вас будет возможность, в случае если вы идёте в магазин, купить мне буханку хлеба...». Или просто сказать: «Купи хлеб». Lambda — это «Купи хлеб» вместо трёхстраничного письма.
Что такое Lambda-выражение
Lambda-выражение (лямбда-выражение) — это анонимная функция, которая позволяет компактно записать реализацию функционального интерфейса. Введено в Java 8.
Синтаксис lambda:
(параметры) -> тело
Стрелка -> читается как «преобразуется в» или «применяется к». Давайте рассмотрим несколько примеров:
() -> System.out.println("Привет")
(x) -> x * 2
(a, b) -> a + b
(a, b) -> {
int sum = a + b;
return sum * 2;
}
Правила синтаксиса:
- Параметры берутся в круглые скобки
- Если параметр один и его тип выводится, скобки можно не ставить:
x -> x * 2 - Если тело — одно выражение, можно не ставить фигурные скобки и return
- Если тело — несколько выражений, используются фигурные скобки и явный return
- Типы параметров обычно выводятся автоматически
Функциональный интерфейс
Lambda-выражения в Java могут использоваться только с функциональными интерфейсами. Функциональный интерфейс — это интерфейс с ровно одним абстрактным методом.
Аналогия: Функциональный интерфейс — как форма для заполнения. У неё есть одно поле (абстрактный метод), и lambda — это то, чем вы заполняете это поле.
Для обозначения функционального интерфейса используется аннотация @FunctionalInterface:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
public class Main {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calculate(3, 5)); // 8
System.out.println(multiply.calculate(3, 5)); // 15
}
}
Аннотация @FunctionalInterface не обязательна, но полезна: компилятор проверит, что в интерфейсе ровно один абстрактный метод.
Стандартные функциональные интерфейсы
Java提供ляет четыре основных функциональных интерфейса в пакете java.util.function. Они покрывают большинство типичных сценариев:
Consumer<T> — принимает значение и ничего не возвращает («потребитель»):
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> print = s -> System.out.println(s);
Consumer<String> shout = s -> System.out.println(s.toUpperCase());
print.accept("Привет"); // Привет
shout.accept("Привет"); // ПРИВЕТ
Consumer<Integer> doubleIt = n -> System.out.println(n * 2);
doubleIt.accept(5); // 10
}
}
Supplier<T> — не принимает аргументов и возвращает значение («поставщик»):
import java.util.function.Supplier;
import java.util.Random;
public class Main {
public static void main(String[] args) {
Supplier<Integer> randomNum = () -> new Random().nextInt(100);
Supplier<String> greeting = () -> "Привет, мир!";
System.out.println(randomNum.get()); // Случайное число 0-99
System.out.println(greeting.get()); // Привет, мир!
}
}
Predicate<T> — принимает значение и возвращает boolean («предикат», условие):
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
System.out.println(isLong.test("Длинное")); // true
System.out.println(isLong.test("А")); // false
}
}
Function<T, R> — принимает значение типа T и возвращает значение типа R («функция»):
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> length = s -> s.length();
Function<Integer, String> toBin = n -> Integer.toBinaryString(n);
System.out.println(length.apply("Привет")); // 6
System.out.println(toBin.apply(10)); // 1010
}
}
Lambda с коллекциями: сортировка
Теперь давайте вернёмся к примеру сортировки и перепишем его с использованием lambda:
import java.util.ArrayList;
import java.util.Collections;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Б");
names.add("Виктор");
names.add("Дима");
Collections.sort(names, (a, b) -> Integer.compare(a.length(), b.length()));
System.out.println(names);
// [Б, Анна, Дима, Виктор]
}
}
Вместо шести строк анонимного класса — одна строка lambda. Вместо Compare(a, b) { return Integer.compare(...) } — просто (a, b) -> Integer.compare(...).
Можно ещё короче, используя Comparator.comparingInt():
import java.util.ArrayList;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Б");
names.add("Виктор");
names.add("Дима");
names.sort(Comparator.comparingInt(String::length));
System.out.println(names);
}
}
Здесь мы использовали method reference (String::length) — об этом подробнее позже.
Lambda с коллекциями: другие примеры
Lambda-выражения широко используются с коллекциями. Вот несколько полезных примеров:
import java.util.ArrayList;
import java.util.Collections;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(2);
numbers.add(8);
numbers.add(1);
numbers.add(9);
Collections.sort(numbers, (a, b) -> b - a);
System.out.println("По убыванию: " + numbers);
Collections.sort(numbers, (a, b) -> a - b);
System.out.println("По возрастанию: " + numbers);
ArrayList<String> words = new ArrayList<>();
words.add("Банан");
words.add("Ананас");
words.add("Арбуз");
words.sort((a, b) -> a.compareTo(b));
System.out.println("Алфавитно: " + words);
words.sort((a, b) -> b.compareTo(a));
System.out.println("Обратный алфавит: " + words);
}
}
Method Reference: ::
Method Reference (ссылка на метод) — это ещё более короткий способ записи lambda. Он используется, когда lambda просто вызывает существующий метод.
Синтаксис: Класс::метод или объект::метод
Вот четыре типа method references:
1. Ссылка на статический метод: ClassName::staticMethod
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Анна", "Борис", "Вика");
names.forEach(name -> System.out.println(name));
names.forEach(System.out::println);
}
}
2. Ссылка на метод экземпляра: ClassName::instanceMethod
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Анна", "Борис", "Вика");
names.forEach(name -> System.out.println(name.toUpperCase()));
names.forEach(name -> System.out.println(String.valueOf(name.length())));
names.forEach(name -> System.out.println(name.length()));
names.forEach(name -> System.out.println(name.isEmpty()));
}
}
3. Ссылка на конструктор: ClassName::new
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Анна", "Борис", "Вика");
List<ArrayList> lists = names.stream()
.map(ArrayList::new)
.collect(Collectors.toList());
System.out.println(lists.size());
}
}
Effectively final
Lambda-выражения могут обращаться к переменным из окружающего метода. Но есть важное ограничение: эти переменные должны быть effectively final — то есть не изменяться после инициализации.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
String prefix = "Привет, "; // effectively final
List<String> names = Arrays.asList("Анна", "Борис", "Вика");
names.forEach(name -> System.out.println(prefix + name));
// Привет, Анна
// Привет, Борис
// Привет, Вика
}
}
Переменная prefix effectively final — она инициализирована и больше не изменяется. Lambda может её использовать.
А вот так нельзя:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
int counter = 0; // НЕ final
List<String> names = Arrays.asList("Анна", "Борис", "Вика");
names.forEach(name -> {
counter++; // Ошибка компиляции!
System.out.println(counter + ": " + name);
});
}
}
Компилятор не позволит изменить переменную counter внутри lambda, потому что это нарушает правило effectively final.
Правило effectively final существует, потому что lambda может выполняться в другом потоке или позже. Если бы lambda могла изменять переменную, это создало бы серьёзные проблемы потокобезопасности.
Lambda vs Анонимный класс
Давайте сравним lambda и анонимный класс для одной и той же задачи:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Анна", "Борис", "Вика");
names.forEach(new java.util.function.Consumer<String>() {
@Override
public void accept(String name) {
System.out.println(name);
}
});
names.forEach(name -> System.out.println(name));
names.forEach(System.out::println);
}
}
Все три варианта делают одно и то же, но второй и третий гораздо короче и чище.
Важные отличия lambda от анонимного класса:
- Lambda может использоваться только с функциональными интерфейсами (с одним абстрактным методом)
- Aнонимный класс может реализовывать интерфейс с любым количеством методов
- Lambda не создаёт отдельный класс — это более эффективно
- Lambda имеет доступ к effectively final переменным
- Aнонимный класс может иметь собственные поля и методы
Практический пример: обработка списка
Давайте создадим программу, которая обрабатывает список студентов с помощью lambda-выражений:
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;
public class StudentProcessor {
public static void main(String[] args) {
ArrayList<String> students = new ArrayList<>();
students.add("Анна");
students.add("Борис");
students.add("Виктор");
students.add("Дима");
students.add("Елена");
students.sort((a, b) -> Integer.compare(a.length(), b.length()));
System.out.println("По длине имени: " + students);
Predicate<String> shortName = name -> name.length() <= 4;
Predicate<String> longName = name -> name.length() > 4;
System.out.print("Короткие имена: ");
for (String name : students) {
if (shortName.test(name)) {
System.out.print(name + " ");
}
}
System.out.println();
System.out.print("Длинные имена: ");
for (String name : students) {
if (longName.test(name)) {
System.out.print(name + " ");
}
}
System.out.println();
}
}
Комбинирование функциональных интерфейсов
Predicate и Function имеют методы для комбинирования, что позволяет создавать сложные условия из простых:
import java.util.function.Predicate;
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
Predicate<Integer> isEvenOrPositive = isEven.or(isPositive);
Predicate<Integer> isNotEven = isEven.negate();
System.out.println(isEvenAndPositive.test(4)); // true
System.out.println(isEvenAndPositive.test(-4)); // false
System.out.println(isEvenOrPositive.test(-3)); // true
System.out.println(isNotEven.test(3)); // true
Function<String, Integer> length = String::length;
Function<Integer, String> timesTen = n -> n + "0";
Function<String, String> composed = length.andThen(timesTen);
System.out.println(composed.apply("Привет")); // 60
}
}
Итоги урока
- Lambda-выражение — это компактная запись анонимной функции: (параметры) -> тело
- Lambda может использоваться только с функциональными интерфейсами (один абстрактный метод)
- Consumer<T> — принимает значение, ничего не возвращает
- Supplier<T> — не принимает аргументов, возвращает значение
- Predicate<T> — принимает значение, возвращает boolean
- Function<T, R> — принимает T, возвращает R
- Method reference (::) — ещё короче запись, когда lambda просто вызывает метод
- Переменные из окружающего кода должны быть effectively final
- Lambda компактнее анонимного класса и эффективнее (не создаёт отдельный класс)
- Predicate и Function поддерживают комбинирование: and(), or(), negate()
Тест по Lambda-выражениям
5 вопросов