1. Ошибка: повторное использование потока
Поток (Stream) в Java — это одноразовый инструмент. Как только вы выполнили терминальную операцию (например, collect(), forEach(), count()), поток закрывается и становится непригодным для дальнейшего использования.
Пример ошибки
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // Всё хорошо
stream.forEach(System.out::println); // Бросит IllegalStateException!
Вывод:
a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
Как избежать
Если нужно выполнить несколько операций — создавайте новый поток каждый раз:
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK
Лайфхак: Поток — как одноразовый стаканчик из столовой: поел — выбросил, новый бери для следующего напитка.
2. Ошибка: изменение коллекции во время стрима
Если вы модифицируете исходную коллекцию во время обхода через поток, получите ConcurrentModificationException. Это частая боль при фильтрации и удалении элементов.
Пример ошибки
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // БУМ!
}
});
Результат:
Exception in thread "main" java.util.ConcurrentModificationException
Как правильно
Используйте методы, которые не модифицируют коллекцию во время обхода, или собирайте результат в новую коллекцию:
names.removeIf(name -> name.startsWith("A")); // OK, removeIf безопасен
// Или с помощью Stream API:
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("A"))
.collect(Collectors.toList());
Запомните: Потоки любят чистоту — не трогайте исходную коллекцию, пока её «едят» потоки.
3. Ошибка: забытый limit при бесконечных потоках
Методы Stream.iterate и Stream.generate могут создавать бесконечные потоки. Если забыть ограничить их размер методом limit, можно зависнуть навсегда (или до OutOfMemoryError).
Пример ошибки
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // Программа не завершится никогда!
Как правильно
Ограничивайте поток методом limit:
Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Выведет числа от 0 до 9
Совет: Бесконечные потоки напоминают сериал без финальной серии: остановиться должен кто-то, и лучше пусть это будете вы!
4. Ошибка: неправильная работа с null
Stream API не любит null. Если в коллекции окажется null, многие операции могут выбросить NullPointerException, особенно если вы вызываете методы на элементах.
Пример ошибки
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // БУМ! На null
.forEach(System.out::println);
Результат:
Exception in thread "main" java.lang.NullPointerException
Как правильно
Фильтруйте null-значения заранее:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
Работа с Optional
Если у вас есть Stream<Optional<T>>, используйте flatMap(Optional::stream) (Java 9+) для «разворачивания»:
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Выведет только "A" и "B"
Совет: Старайтесь не добавлять null в коллекции. Если уж без них никак, фильтруйте их на старте.
5. Ошибка: потеря порядка элементов
Некоторые операции (например, flatMap, concat) могут влиять на порядок элементов, особенно если вы работаете с параллельными потоками или объединяете разные коллекции.
Пример ошибки
List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 — порядок сохраняется
// Но если использовать параллельные потоки:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // Порядок может быть неожиданным!
Как правильно
Если порядок важен — избегайте параллельных потоков или используйте методы, гарантирующие порядок (forEachOrdered):
par.forEachOrdered(System.out::print);
Факт: Stream API по умолчанию сохраняет порядок, если только вы сами его не «сломали».
6. Ошибка: неэффективное объединение коллекций
Для операций над множествами лучше использовать Set, а не List, чтобы избежать дубликатов и повысить производительность. Также неэффективно писать вложенные циклы там, где можно использовать flatMap.
Пример ошибки
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");
// Неэффективно:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
if (!union.contains(s)) { // contains у List — O(n)
union.add(s);
}
}
// Эффективно с Set и Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toSet());
Или для разности:
// Неэффективно:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, но если бы вы делали через stream и list.contains — было бы медленно
// Эффективно:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
.filter(s -> !set2.contains(s))
.collect(Collectors.toList());
Совет: Для работы с уникальными элементами всегда используйте Set. Для больших коллекций — избегайте вложенных циклов, отдавайте предпочтение stream + flatMap.
7. Ошибка: повторное использование терминальных операций
Иногда хочется «разделить» поток на две ветки, например, собрать элементы в список и параллельно посчитать их количество. Но поток «одноразовый», и вторая операция вызовет ошибку.
Пример ошибки
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // БУМ! IllegalStateException
Как правильно
Если нужно использовать поток несколько раз — сначала соберите его в коллекцию:
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();
Совет: Если хочется и рыбку съесть, и на поток сесть — сначала соберите в коллекцию.
8. Ошибка: забытый close() у ресурсов
Если вы используете потоки, связанные с ресурсами (например, Files.lines(Path)), не забывайте закрывать их после использования, чтобы не было утечек.
Пример ошибки
Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, но ресурс не закрыт!
Как правильно
Используйте try-with-resources:
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
lines.forEach(System.out::println);
}
Совет: Потоки с файлами — как чайник: вскипятил — выключи!
9. Ошибка: некорректная обработка Optional и null в flatMap
При работе с коллекциями типа List<Optional<T>> многие используют map(Optional::get), что приводит к исключениям, если внутри Optional пусто.
Пример ошибки
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
.map(Optional::get) // БУМ! NoSuchElementException
.forEach(System.out::println);
Как правильно
Используйте flatMap(Optional::stream) (Java 9+):
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Только "A"
Совет: Optional — это не просто обёртка, а инструмент для безопасной работы с отсутствием значения.
10. Ошибка: неверно реализованный equals/hashCode
Операции над множествами (Set, distinct, объединение, пересечение) зависят от корректной реализации методов equals и hashCode.
Пример ошибки
class Student {
String name;
int age;
// Нет equals и hashCode!
}
Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, хотя должны быть одинаковые!
Как правильно
Переопределяйте equals и hashCode для своих классов, если планируете использовать их в коллекциях Set, Map или для операций distinct.
11. Ошибка: использование stream после collect
После терминальной операции (например, collect) поток закрывается. Любая попытка продолжить работу с этим же потоком приведёт к ошибке.
Пример ошибки
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!
Как правильно
Создавайте новый поток от коллекции:
list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);
12. Ошибка: неправильное использование parallelStream
Многие думают, что parallelStream() всегда ускоряет обработку. На самом деле, для небольших коллекций или простых операций параллелизм может только замедлить выполнение из-за накладных расходов на управление потоками.
Пример ошибки
List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Может быть медленнее, чем обычный stream()
Как правильно
Используйте parallelStream только для действительно больших коллекций или тяжёлых вычислений, особенно если операции чистые (без побочных эффектов) и ассоциативные.
13. Ошибка: забытый терминальный оператор
Промежуточные операции (map, filter, flatMap и т.д.) не выполняются до тех пор, пока не вызван терминальный оператор (collect, forEach, count и т.д.).
Пример ошибки
Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// Ничего не произойдёт!
Как правильно
Обязательно вызывайте терминальный оператор:
stream.forEach(System.out::println);
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ