Современная Java (Java 8+)
Java 8 (2014) изменила то, как выглядит идиоматичный код: поведение стали передавать не громоздкими анонимными классами, а лямбдами, коллекции обрабатывать декларативным Stream API, а отсутствие значения выражать типом Optional, а не null. Это не набор синтаксического сахара, а другой способ думать о данных и вычислениях.
Через всю тему проходит одна связка. Функциональный интерфейс (ровно один абстрактный метод) — это тип, к которому привязывается лямбда. Стримы строятся из лямбд и ленивы: промежуточные операции ничего не считают, пока не придёт терминальная. map и flatMap различаются одним уровнем вложенности — перепутать их значит получить Optional<Optional<T>> или Stream<Stream<T>>. Optional делает отсутствие явным, но только для возвращаемых типов. Параллельные стримы дают ускорение почти даром — и так же легко молча ломают корректность. А обобщения держат всё это типобезопасным на этапе компиляции, хотя в рантайме их уже нет из-за стирания. Полная карта — в слоях ниже.
Карта темы
- Лямбда-выражения — компактная реализация одного метода; замыкание над effectively-final переменными;
this— окружающий экземпляр. - Функциональные интерфейсы — ровно один абстрактный метод,
@FunctionalInterface, готовыеFunction/Predicate/Supplier/Consumer. - Stream API — ленивый конвейер: источник, промежуточные и терминальная операции; одноразовость; не мутирует источник.
- map против flatMap —
mapпреобразует элемент,flatMapещё и разворачивает один уровень вложенности. - Класс Optional — явное «значение может отсутствовать» для возвращаемых типов;
map/orElse/ifPresentвместо слепогоget. - Параллельные стримы — общий
ForkJoinPool, когда помогает и когда молча ломает корректность. - Обобщения — типобезопасность на компиляции, стирание типов, границы и wildcards
? extends/? super.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Захватить и переприсвоить локальную переменную внутри лямбды | Не компилируется — захватываемая переменная обязана быть effectively-final |
| Считать промежуточные операции стрима немедленными | Пока нет терминальной операции, не выполнится ничего; кажется, что «код не работает» |
map над функцией, возвращающей Optional/Stream | Вложенность Optional<Optional<T>> — на самом деле был нужен flatMap |
Слепой get() на Optional без проверки | NoSuchElementException — тот же NullPointerException, только отложенный |
Использовать Optional для полей и параметров | Не по назначению: лишняя обёртка, неудобно вызывающему |
Перевести стрим в parallelStream «ради скорости» | Гонки при мутации общего состояния; неверный результат на не-нейтральном seed |
Ждать аргумент обобщённого типа в рантайме (instanceof List<String>) | Не компилируется — стирание убрало тип элемента |
Значение для собеседований
Современную Java спрашивают почти на любом собеседовании уровня middle и выше — но проверяют не синтаксис, а модель. Кандидат, который говорит «стрим ленив, выполнение запускает только терминальная операция» и «обобщения стираются, в рантайме их нет», сразу отделяется от того, кто заучил список методов.
Что обычно проверяют:
- Что лямбда захватывает только effectively-final переменные и что её
this— окружающий объект, а не сама лямбда. - Почему функциональный интерфейс — это ровно один абстрактный метод и почему именно это позволяет к нему привязать лямбду.
- Ленивость стрима, разницу промежуточных и терминальных операций, одноразовость конвейера.
mapпротивflatMap— ровно по одному уровню вложенности.- Зачем нужен
Optionalи почему он только для возвращаемых типов. - Когда параллельный стрим ускоряет, а когда ломает корректность.
- Стирание типов и его следствия (
new T(),instanceof List<String>, столкновение перегрузок).
Типичный неверный ответ: «parallelStream всегда быстрее». Это повод обсудить накладные расходы на малых источниках, опасность блокирующих задач в общем ForkJoinPool и то, что неассоциативный аккумулятор или не-нейтральный seed молча дают неверный результат.