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);
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ