1. Проблема імперативного підходу
Почнімо з класичного завдання. Припустімо, у нас є список користувачів:
List<User> users = ...; // вже заповнений
Припустімо, нам потрібно отримати список імен усіх повнолітніх користувачів (від 18 років). Як ми робили це раніше?
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
names.add(user.getName());
}
}
На перший погляд усе просто, але зверніть увагу на «технічні» кроки: створити порожній список для результату, пройтися всіма елементами, перевірити умову, додати значення.
Якщо завдання ускладнюється — наприклад, потрібно отримати відсортований за абеткою список унікальних e‑mail‑адрес усіх користувачів, чиї імена починаються з літери «А», — код швидко розростається. Фільтрація, витягування полів, усунення дублікатів, сортування, можливо, перетворення формату e‑mail‑адрес тощо. У підсумку замість короткої, зрозумілої логіки ви отримуєте гору шаблонного коду, яку важко читати й підтримувати.
Мінуси такого підходу у двох словах: багато повторюваного коду, висока ймовірність припуститися помилки (забути про сортування або неправильно обробити дублікати) і складність комбінування операцій. Це і є імперативний стиль: ви крок за кроком описуєте компʼютеру, як виконувати завдання, а не що ви хочете отримати.
2. Stream API — декларативний стиль
Із виходом Java 8 з’явився Stream API — інструмент, що дає змогу працювати з колекціями у стилі «що потрібно зробити», а не «як саме це зробити». Такий підхід називають декларативним.
А що таке Stream взагалі?
Це не окрема колекція, а радше потік даних: послідовність елементів, що проходять через ланцюжок операцій — фільтрацію, перетворення, сортування, збирання результату тощо. Потік нічого не зберігає: він лише «протягує» дані через конвеєр. Операції можна поєднувати майже як кубики LEGO: зібрали ланцюжок і запустили.
Приклад:
List<String> names = users.stream() // створюємо потік зі списку користувачів
.filter(u -> u.getAge() >= 18) // залишаємо лише тих, кому виповнилося 18
.map(User::getName) // перетворюємо User → String (беремо імʼя)
.collect(Collectors.toList()); // збираємо результат у список
І все — одним рядком ми повністю описали завдання: «візьміть користувачів, відфільтруйте за віком, дістаньте імена, зберіть у список». Не потрібно вручну писати цикли, створювати проміжні змінні й стежити, щоб нічого не забути.
Stream API працює як заводський конвеєр: ви визначаєте етапи обробки, а дані самі проходять через них. Виглядає охайно й компактно, а читається в рази простіше.
3. Переваги Stream API
- Стислість і читабельність. Порівняйте два підходи — імперативний і декларативний — на одному й тому самому завданні. Одразу видно, який із них легше читати й підтримувати.
- Проста композиція операцій. Операції легко «складати» в ланцюжок: filter, map, sorted, collect — і все це в одній зв’язній послідовності.
- Менше помилок. Stream API звільняє від рутини: не потрібно вручну керувати проміжними колекціями й циклами — менший шанс забути крок або зробити помилку індексації.
- Можливість паралелізму. Легко масштабувати обробку на кілька ядер, просто викликавши parallelStream() замість stream(). Подробиці — згодом.
- Сучасний стиль програмування. Ідеї функціонального програмування (композиція функцій, відсутність явних циклів) підвищують виразність і цінуються на ринку.
Коротка історія Stream API
Stream API з’явився у Java 8 (2014) і став якісним стрибком для мови. До того часу будь-яка нетривіальна операція над колекціями вимагала багато допоміжного коду, тоді як інші платформи вже пропонували декларативні підходи (map, filter, reduce тощо).
Відтоді Stream API — стандарт де‑факто для обробки колекцій у Java. Якщо ви хочете писати сучасний Java‑код — без нього нікуди.
4. Сфери застосування Stream API
- Фільтрація: вибрати лише потрібні елементи (наприклад, усі користувачі старші за 18 років).
- Перетворення: витягнути поле або створити новий обʼєкт (наприклад, список імен користувачів).
- Агрегація: порахувати суму, середнє, кількість, максимум/мінімум.
- Сортування: упорядкувати елементи за потрібною ознакою.
- Групування: розбити елементи за категоріями.
- Збирання результату: зібрати в список, множину, мапу, рядок тощо.
Приклади завдань:
- Отримати список e‑mail‑адрес усіх користувачів, чиї імена починаються на «А».
- Порахувати середній вік користувачів.
- Знайти першого користувача з певною e‑mail‑адресою.
- Зібрати всі унікальні міста проживання в одну множину.
5. Корисні нюанси
Як працює потік
[User1] --\
[User2] ---|--> [ filter ] --> [ map ] --> [ collect ] --> List<String>
[User3] --/
1. filter — пропускає лише користувачів, які задовольняють умову.
2. map — перетворює користувача, наприклад, на e‑mail‑адресу.
3. collect — збирає e‑mail у потрібну колекцію.
Синтаксис: як створити потік
- З колекції: list.stream() — потік елементів колекції.
- З масиву: Arrays.stream(array)
- З окремих значень: Stream.of("a", "b", "c")
6. Типові помилки під час переходу на Stream API
Помилка № 1: спроба змінити колекцію всередині потоку. Stream API не призначений для модифікації вихідної колекції (наприклад, не варто робити list.remove() у тілі forEach). Для видалень використовуйте, приміром, removeIf на колекції до або після обробки.
Помилка № 2: плутанина між колекцією та потоком. Потік — це не колекція! Потік «проходить» елементами один раз, після чого закривається. Якщо потрібно знову обробити дані — створіть новий stream().
Помилка № 3: надто складні ланцюжки. Не перетворюйте вираз на «монстра» з десятка операцій. Якщо ланцюжок стає довгим — розбийте його на кілька кроків із проміжними змінними для читабельності та налагодження.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ