1. Лямбда-выражения и коллекции
Вспомним, как выглядела обработка коллекций до появления лямбда-выражений. Допустим, у нас есть список строк, и мы хотим вывести их на экран:
List<String> list = Arrays.asList("кот", "пёс", "ёж");
for (String s : list) {
System.out.println(s);
}
Всё просто, но если мы хотим, например, удалить из списка все пустые строки, приходится писать цикл с условием, а иногда и использовать итератор (иначе будет ConcurrentModificationException). Или, скажем, сортировка по длине строки:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Даже на такой простой задаче — уже 5 строк кода и куча «шумных» скобок. Как оптимизировать? Ответ вы уже знаете: лямбда-выражения.
Применение лямбда-выражений в методах коллекций
С Java 8 интерфейсы коллекций получили новые методы, которые принимают функциональные интерфейсы — а значит, мы можем передавать туда лямбда-выражения. Вот самые популярные из них:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
Пример: forEach
Вывести все элементы списка на экран (старый способ):
for (String s : list) {
System.out.println(s);
}
Теперь — с лямбдой:
list.forEach(s -> System.out.println(s));
Или ещё короче, если хочется поиграть в «гуру Java»:
list.forEach(System.out::println); // method reference, разберём позже
Пример: removeIf
Удалить все пустые строки из списка:
List<String> animals = new ArrayList<>(Arrays.asList("кот", "", "пёс", "ёж", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [кот, пёс, ёж]
Пример: sort
Сортировка списка по длине строки:
List<String> animals = Arrays.asList("кот", "пёс", "ёж", "слон");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [ёж, кот, пёс, слон]
Пример: replaceAll
Преобразовать все строки к верхнему регистру:
List<String> animals = new ArrayList<>(Arrays.asList("кот", "пёс", "ёж"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [КОТ, ПЁС, ЁЖ]
2. Stream API и лямбда-выражения
С выходом Java 8 появился Stream API — мощный инструмент для обработки коллекций в функциональном стиле. Потоки позволяют фильтровать, преобразовывать, сортировать, собирать коллекции с помощью цепочек методов. И все эти методы принимают лямбда-выражения!
Важно: Полный разбор Stream API будет позже, сейчас — только базовые примеры для понимания роли лямбд.
Пример: фильтрация
Оставить только строки длиннее 3 символов:
List<String> animals = Arrays.asList("кот", "слон", "ёж", "крокодил");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // слон, крокодил
Пример: преобразование (map)
Сделать все строки заглавными:
List<String> animals = Arrays.asList("кот", "слон", "ёж");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [КОТ, СЛОН, ЁЖ]
Пример: сортировка
Получить отсортированный по длине список (не меняя исходный):
List<String> animals = Arrays.asList("кот", "слон", "ёж", "крокодил");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [ёж, кот, слон, крокодил]
3. Сравнение с анонимными классами
Давайте сравним, как выглядел бы тот же код с анонимным классом и с лямбдой.
Сортировка по длине строки
Анонимный класс:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Лямбда-выражение:
animals.sort((a, b) -> a.length() - b.length());
Вывод:
Лямбда-выражение экономит кучу строк и делает код более читабельным. Меньше скобок, меньше шума — больше сути!
Удаление пустых строк
Анонимный класс:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
Лямбда-выражение:
animals.removeIf(s -> s.isEmpty());
4. Практика: короткие задачи с лямбда-выражениями
Давайте попробуем на практике применить лямбда-выражения в мини-программе для работы со списком пользователей.
Пример 1: Фильтрация пользователей по возрасту
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Алиса", 17),
new User("Боб", 25),
new User("Чарли", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Боб (25)
Пример 2: Сортировка пользователей по имени
List<User> users = Arrays.asList(
new User("Алиса", 17),
new User("Боб", 25),
new User("Чарли", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Алиса (17), Боб (25), Чарли (15)]
Пример 3: Преобразование списка пользователей в список имён
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Алиса, Боб, Чарли]
Пример 4: Удалить всех несовершеннолетних
List<User> users = new ArrayList<>(Arrays.asList(
new User("Алиса", 17),
new User("Боб", 25),
new User("Чарли", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Боб (25)]
5. Полезные нюансы
Особенности: область видимости и «эффективно final» переменные
Лямбда-выражение может использовать переменные из внешнего метода, но только если они final или «эффективно final» (то есть не изменяются после инициализации).
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
Если вы попробуете изменить minAge после использования в лямбде — компилятор выдаст ошибку.
Таблица: основные методы коллекций и стримов с лямбда-выражениями
| Метод коллекции/стрима | Что делает | Тип лямбда-выражения | Пример |
|---|---|---|---|
|
Для каждого элемента | |
|
|
Удаляет элементы по условию | |
|
|
Сортирует элементы | |
|
|
Заменяет каждый элемент | |
|
|
Фильтрует поток | |
|
|
Преобразует элементы | |
|
|
Итерация по потоку | |
|
|
Сортировка в потоке | |
|
7. Типичные ошибки
Ошибка №1: Лямбда слишком длинная. Если внутри лямбда-выражения у вас уже 5 строк кода, условия, циклы и try-catch — скорее всего, стоит вынести этот код в отдельный метод. Лямбды хороши для короткой логики.
Ошибка №2: Использование изменяемых переменных. Если вы пытаетесь внутри лямбды изменить переменную из внешнего метода (например, счётчик), компилятор не даст этого сделать. Переменная должна быть final или эффективно final.
Ошибка №3: Забыли, что методы коллекций/стримов не всегда меняют исходную коллекцию. Например, stream().filter(...) не изменяет исходный список, а возвращает новый поток. Если хотите получить коллекцию — используйте collect(Collectors.toList()).
Ошибка №4: Лямбда-выражение не подходит по типу. Если метод принимает, например, Comparator<T>, а вы пытаетесь передать лямбду с одним параметром (а не двумя) — будет ошибка компиляции.
Ошибка №5: Теряете читаемость при вложенных лямбдах. Если у вас цепочка из map, filter, forEach, и внутри каждой лямбды ещё одна лямбда — код становится нечитаемым. В таких случаях лучше разбить выражения на отдельные шаги или вынести части в методы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ