Стримы и лямбды появились в Java 8 и с тех пор есть в каждом проекте. На собесе просят написать группировку на стримах, объяснить ленивость и разницу map и flatMap, а на senior спрашивают про parallel и цену абстракции.
Функциональные интерфейсы и лямбды
Функциональный интерфейс имеет ровно один абстрактный метод (аннотация @FunctionalInterface проверяет это). Лямбда это компактная реализация такого интерфейса.
| Интерфейс | Метод | Смысл |
|---|---|---|
Supplier<T> | T get() | дать значение |
Consumer<T> | void accept(T) | использовать значение |
Function<T, R> | R apply(T) | преобразовать |
Predicate<T> | boolean test(T) | проверить |
UnaryOperator<T>, BinaryOperator<T> | T apply(T), T apply(T, T) | операции над одним типом |
Для примитивов есть IntFunction, ToIntFunction, IntPredicate и другие: без упаковки.
Лямбда может читать локальные переменные, только если они effectively final (не меняются после инициализации). Причина: лямбда захватывает копию значения, и менять оригинал было бы обманом. Поля класса менять можно.
Ссылки на методы: String::length (метод экземпляра на аргументе), System.out::println (на конкретном объекте), Integer::parseInt (статический), ArrayList::new (конструктор).
Как устроен стрим
Источник → промежуточные операции → терминальная операция.
Map<String, Long> byCity = users.stream()
.filter(u -> u.age() >= 18)
.collect(Collectors.groupingBy(User::city, Collectors.counting()));
- Промежуточные (
filter,map,flatMap,sorted,distinct,peek,limit) ленивые: только описывают конвейер. - Терминальные (
collect,forEach,reduce,count,findFirst,anyMatch,toList) запускают обработку. - Элементы проходят конвейер по одному, вертикально: первый элемент проходит
filterиmap, потом второй. Исключение:sortedиdistinctс состоянием,sortedсначала собирает всё. - Короткое замыкание:
findFirst,anyMatch,limitостанавливают обработку, как только ответ известен. - Стрим одноразовый: второй терминальный вызов бросает
IllegalStateException.
Без терминальной операции ничего не выполнится, даже peek с печатью.
map и flatMap
map превращает каждый элемент в один новый, flatMap в стрим элементов и склеивает их:
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3));
nested.stream().flatMap(List::stream).toList(); // [1, 2, 3]
Коллекторы
toList()(метод стрима, Java 16) возвращает неизменяемый список,Collectors.toList()на практике отдаёт изменяемыйArrayList, но спецификация тип и изменяемость не обещает.toMap(k, v)падает сIllegalStateExceptionна дубле ключа, если не дать функцию слияния третьим аргументом.groupingBy(classifier, downstream):counting(),summingInt,mapping,toSet().partitioningBy(predicate): две группы,trueиfalse.joining(", ")для строк.
Optional
Контейнер «значение или ничего» для возвращаемых значений. map, filter, orElse, orElseGet, orElseThrow. Важно: orElse(x) вычисляет x всегда, orElseGet(() -> x) только если пусто. Optional не используют для полей, параметров и коллекций, и никогда не возвращают null вместо Optional.
Параллельные стримы
parallelStream() делит работу на общем ForkJoinPool.commonPool(). Помогает на больших объёмах CPU-работы с хорошо делимым источником (ArrayList, массив). Вредит на маленьких данных, блокирующих операциях (заберут общий пул) и на LinkedList. Лямбды не должны менять общее состояние: forEach(list::add) в параллели теряет элементы.
Частые ошибки
- Побочные эффекты в
mapиpeek. toMapбез функции слияния на данных с дублями.orElse(expensiveCall()), который вызывается всегда.- Повторное использование стрима.
Что спросят на собесе
- Что такое функциональный интерфейс и какие стандартные знаете?
- Почему переменная в лямбде должна быть effectively final?
- Промежуточные и терминальные операции, что такое ленивость?
- Разница
mapиflatMap,orElseиorElseGet. - Когда parallelStream ускоряет, а когда вредит?