Операции Stream API
Собрал стрим из трёх строк, поставил peek(seen::add) - хук, который просто складывает проходящие элементы в список, - и позвал count(). Результат: count вернул 3, а список остался ПУСТОЙ. Добавил в цепочку filter - и тот же peek вдруг увидел все три элемента.
Вот вся модель стримов в одном замере: конвейер ленив и выполняет ровно столько, сколько нужно для ответа. Кто думает о стриме как о модном цикле, сыплется на первом же вопросе «что напечатает».
// Формулировки: «чем промежуточные операции отличаются от терминальных?», «map или flatMap?», «можно ли пройти по стриму дважды?».
Ленивость: ничего не происходит до терминальной операции
Разберём тот замер. Конвейер устроен так: источник, затем промежуточные операции (filter, map, sorted и другие), затем ровно ОДНА терминальная (count, collect, forEach). Промежуточная операция ничего не выполняет, она только дописывает шаг в план; запускает всё терминальная. Собрал цепочку и не поставил терминальную - не выполнилось вообще ничего, я это тоже проверил: peek промолчал.
Почему count() пропустил peek: чтобы посчитать элементы, ему хватило размера источника - список знает свою длину, гонять элементы незачем. А как только появился filter, размер стал заранее неизвестен (фильтр может выбросить что угодно), и элементы честно потекли через peek. Отсюда правило: peek годится для отладки, но логику в него класть нельзя, она может не выполниться.
Ленивость даёт и короткое замыкание - остановку, как только ответ уже ясен. Я взял миллион чисел по порядку и искал первое больше 500 000 через findFirst: конвейер просмотрел 500 001 элемент и остановился, а не миллион.
var seen = new ArrayList<String>();
long c = list.stream().peek(seen::add).count();
// c = 3, seen = [] - элементы не текли
long c2 = list.stream().peek(seen::add).filter(s -> true).count();
// c2 = 3, seen = [a, b, c] - размер стал неизвестен, потекли- промежуточная / терминальная
- шаг плана, ничего не выполняет / запуск всего конвейера
- короткое замыкание
- остановка, как только ответ известен: findFirst, anyMatch, limit
takeWhile против filter: разница в тысячи раз
Взял миллион чисел по возрастанию и посчитал, сколько из них выбирает условие «меньше ста», двумя способами. Оба дали 99. Но takeWhile просмотрел 100 элементов, а filter - все 1 000 000.
Причина простая. filter обязан проверить каждый элемент: он не знает, что дальше не встретится подходящий. takeWhile берёт элементы с начала, пока условие держится, и на первом нарушении закрывает источник. На отсортированных данных это разница в десять тысяч раз, на неотсортированных takeWhile просто даст другой ответ - он берёт префикс, а не всё подходящее.
// Рядом живёт dropWhile: он наоборот выбрасывает начальный кусок, пока условие выполняется, и отдаёт весь остаток. Обе операции появились в Java 9.
sorted.stream().takeWhile(x -> x < 100).count();
// результат 99, просмотрено 100
sorted.stream().filter(x -> x < 100).count();
// результат 99, просмотрено 1 000 000- takeWhile / dropWhile
- взять начальный кусок по условию / отбросить его
map, flatMap и одноразовость стрима
map преобразует один в один: был элемент - стал элемент. flatMap применяют, когда функция возвращает не значение, а целый стрим значений: все эти стримы склеиваются в один плоский поток. Практический признак: если после map получился Stream<List<Позиция>>, а нужен Stream<Позиция>, значит нужен был flatMap.
Стрим одноразовый. Позвал терминальную операцию во второй раз на том же объекте - получил IllegalStateException с текстом stream has already been operated upon or closed. Нужно пройтись дважды - создавай новый стрим из источника, это дёшево. Сам источник при этом цел: стримы его не меняют, filter возвращает новые данные.
// А вот менять источник во время обхода нельзя. Я добавил элемент в список прямо внутри forEach по этому же списку и получил ConcurrentModificationException. Иногда вместо исключения выходит тихий мусор - поведение тут не определено, полагаться не на что.
- flatMap
- функция вернула стрим, все стримы склеились в один
- одноразовость
- вторая терминальная операция на том же стриме бросает исключение
Как отвечать: «Чем map отличается от flatMap?»
map преобразует один к одному: применил функцию, получил ровно один элемент на каждый входной. flatMap нужен, когда функция возвращает не значение, а стрим значений: каждый элемент разворачивается в поток, и все потоки склеиваются в один плоский. Практический признак у меня такой: если после map получился Stream<List<Item>>, а нужен Stream<Item> - это сигнал заменить на flatMap(list -> list.stream()). Тот же приём работает у Optional: flatMap для функций, которые сами возвращают Optional, иначе получается матрёшка - я проверял, map(Optional::of) честно даёт Optional[Optional[Казань]], а flatMap разворачивает до Optional[Казань]. Суть одна: map кладёт результат в контейнер как есть, flatMap распаковывает вложенный контейнер.
Почему это сильный ответ: правило дано через практический признак с типом-матрёшкой, и показано, что приём общий для Stream и Optional - это понимание идеи, а не заучивание одного метода.
На чём валят
- −Конвейер без терминальной операции. «Стрим не работает» - он и не запускался, промежуточные операции только строят план.
- −Логика внутри peek. Под count() без фильтров он у меня не выполнился ни разу: список остался пустым.
- −Вторая терминальная операция на том же стриме. IllegalStateException, стрим одноразовый.
- −Менять список внутри его же стрима. ConcurrentModificationException или тихая порча данных.
- −sorted() и distinct() на бесконечном источнике. Им нужно увидеть все элементы, поэтому конвейер копит поток в памяти до отказа.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 18, остальные разбираются в тренажёре.
- Что такое короткое замыкание (short-circuit) операции стрима?A)Автоматическое прекращение стрима при первом же исключении в одной из промежуточных операцийB)Оптимизация, при которой стрим выполняет все операции параллельно и берёт первый готовый результатC)Терминал/операция может завершиться, не обработав все элементы (findFirst, anyMatch, limit)D)Механизм, замыкающий два стрима в кольцо, чтобы элементы бесконечно циркулировали между ними
показать ответ и разбор
+C)Терминал/операция может завершиться, не обработав все элементы (findFirst, anyMatch, limit)// разбор: Короткозамыкающие операции могут дать результат, не обработав ВЕСЬ источник: anyMatch/allMatch/noneMatch (останавливаются, как только ответ ясен), findFirst/findAny, а из промежуточных — limit (берёт первые N). Благодаря ленивости стрим протягивает элементы по одному и прекращает, как только достаточно. Это, в частности, позволяет работать с БЕСКОНЕЧНЫМИ стримами (iterate/generate) при наличии limit/findFirst. Без короткого замыкания такие стримы висли бы вечно.
- Изменяет ли stream() исходную коллекцию?A)Да, операции map и filter изменяют элементы прямо в исходной коллекции на месте, экономя памятьB)Да, sorted() сортирует исходный список, поэтому после стрима его порядок оказывается изменённымC)Зависит от терминальной операции: collect не меняет источник, а forEach его перезаписываетD)Нет — стрим не мутирует источник, а порождает новый результат
показать ответ и разбор
+D)Нет — стрим не мутирует источник, а порождает новый результат// разбор: Стрим НЕ модифицирует источник: filter/map/sorted и т.д. строят новый результат, а исходная коллекция остаётся как была (в отличие от, например, Collections.sort, которая сортирует список на месте). Изменять источник во время работы стрима вообще нельзя — это ведёт к неопределённому поведению/CME. Функциональный стиль: на входе — данные, на выходе — НОВЫЙ результат, без побочных эффектов на источник. Побочные эффекты в лямбдах стрима (особенно параллельного) — антипаттерн.
- Что делает distinct() и на чём основано сравнение элементов?A)Убирает дубликаты, сравнивая элементы по equals/hashCodeB)Убирает дубликаты, сравнивая элементы по ссылкам через ==, поэтому равные по значению остаютсяC)Сортирует элементы и удаляет только соседние повторы, идущие подряд в отсортированном порядкеD)Оставляет только элементы, встретившиеся более одного раза, отбрасывая уникальные значения
показать ответ и разбор
+A)Убирает дубликаты, сравнивая элементы по equals/hashCode// разбор: distinct() оставляет по одному экземпляру каждого элемента, определяя равенство через equals (и hashCode). Поэтому у объектов должны быть корректно переопределены эти методы, иначе «одинаковые» объекты не схлопнутся. В последовательном стриме сохраняется порядок первых вхождений; в параллельном distinct дороже (нужна согласованность). Для распознавания дубликатов по конкретному полю обычно применяют приём с filter + Set (seen) или toMap/groupingBy по ключу.
- Чем отличаются sorted() и sorted(Comparator) и когда они выполняются?A)Sorted() сортирует по убыванию, а sorted(Comparator) — по возрастанию, аргумент задаёт направлениеB)Без аргумента — естественный порядок (Comparable); это ленивая, но stateful-операцияC)Sorted() выполняется немедленно при вызове, а sorted(Comparator) откладывается до терминальной операцииD)Sorted() работает только с числами, а для объектов обязателен sorted(Comparator) с явным правилом
показать ответ и разбор
+B)Без аргумента — естественный порядок (Comparable); это ленивая, но stateful-операция// разбор: sorted() сортирует по естественному порядку (элементы должны быть Comparable, иначе ClassCastException в рантайме), sorted(Comparator) — по заданному правилу. Обе — промежуточные и ленивые, НО «с состоянием» (stateful): чтобы отсортировать, стрим обязан СНАЧАЛА собрать все элементы, поэтому они ломают чистую поэлементную конвейерность и не работают на бесконечных стримах без предварительного limit. Это стоит памяти на буфер. Дешевле сортировать после filter, уменьшив число элементов.
- Что делают limit(n) и skip(n)?A)Limit оставляет каждый n-й элемент, а skip удаляет каждый n-й, прореживая стрим с заданным шагомB)Limit ограничивает время выполнения стрима n миллисекундами, а skip задерживает старт на n мсC)limit берёт первые n элементов, skip пропускает первые nD)Limit и skip делают одно и то же — обрезают стрим до n элементов, отличаясь лишь стороной обрезки массива
показать ответ и разбор
+C)limit берёт первые n элементов, skip пропускает первые n// разбор: limit(n) оставляет первые n элементов и является короткозамыкающей — прекращает обработку, набрав n (позволяет усечь бесконечный стрим). skip(n) отбрасывает первые n и пропускает дальше остаток. Вместе (skip(a).limit(b)) их используют для пагинации. На УПОРЯДОЧЕННОМ стриме «первые n» детерминированы; на неупорядоченном/параллельном порядок может быть любым, а limit/skip дороже (нужна координация). Оба — промежуточные операции.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.