Как использование Stream влияет на читаемость и производительность кода в Java
Что проверяют
Есть ли у вас взвешенное мнение, а не позиция «потоки всегда лучше» или «потоки всегда медленнее».
Как отвечать
Читаемость. Потоки выигрывают там, где идёт цепочка преобразований: отфильтровать, преобразовать, сгруппировать, собрать. Запись описывает намерение, а не механику обхода, и такой код короче и меньше подвержен ошибкам с индексами.
Проигрывают они в двух случаях: когда нужен простой перебор с побочным эффектом (обычный цикл честнее) и когда внутри сложная логика с досрочным выходом, обработкой исключений или несколькими переменными состояния. Втиснутая в поток такая логика читается хуже цикла.
Производительность. Для обычных операций над коллекцией цикл, как правило, немного быстрее: у потока есть накладные расходы на построение конвейера и на виртуальные вызовы лямбд. Разница обычно в пределах, которые не имеют значения на фоне обращений к базе и сети.
Где разница становится существенной:
- автоупаковка:
Stream<Integer>заметно дороже, чемIntStream. Для чисел берут примитивные потоки с ихsum,average,range; - короткое замыкание:
findFirstиanyMatchостанавливают обработку, и на больших источниках поток может выиграть у наивного цикла, обходящего всё; - ленивость: элементы проходят конвейер по одному, промежуточных коллекций не создаётся. Цепочка из трёх фильтров не делает трёх проходов.
Про parallelStream надо сказать отдельно, потому что его применяют неправильно: он использует общий пул ForkJoinPool, поэтому оправдан только для вычислительной работы на больших объёмах и категорически не подходит для блокирующих операций, иначе одна задача займёт общий пул на всё приложение.
Ещё один практический минус: отладка. Стек вызовов внутри конвейера трудно читать, точки останова работают хуже, а peek для подглядывания может вообще не выполниться, если конвейер оптимизирован.
Вывод: выбирать по читаемости конкретного места, а при сомнениях в скорости измерять на JMH, а не спорить.
Где спотыкаются
Ставят parallelStream ради ускорения без замера.
Используют Stream<Integer> там, где нужен IntStream.
Пишут побочные эффекты внутри map.
Считают потоки универсально медленнее и избегают их из принципа.