JavaRush /Курси /JAVA 25 SELF /Розбір типових помилок під час роботи зі Stream API

Розбір типових помилок під час роботи зі Stream API

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); // Виняток!

Як правильно

Створюйте новий потік на основі колекції:

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
Опитування
Об’єднання і проєкції, рівень 32, лекція 4
Недоступний
Об’єднання і проєкції
Stream API: об’єднання і проєкції
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ