JavaRush /Курсы /JAVA 25 SELF /Разбор типичных ошибок при работе с потоками

Разбор типичных ошибок при работе с потоками

JAVA 25 SELF
32 уровень , 4 лекция
Открыта

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);
1
Задача
JAVA 25 SELF, 32 уровень, 4 лекция
Недоступна
Опасная очистка базы данных с возможными "пустыми" записями 😼
Опасная очистка базы данных с возможными "пустыми" записями 😼
1
Задача
JAVA 25 SELF, 32 уровень, 4 лекция
Недоступна
Волшебный свиток с однократным использованием заклинаний 📜
Волшебный свиток с однократным использованием заклинаний 📜
1
Опрос
Объединения и проекции, 32 уровень, 4 лекция
Недоступен
Объединения и проекции
Stream API: объединения и проекции
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ