1. Перетворення List → Set і навпаки
Згадаймо, навіщо взагалі потрібні перетворення колекцій. Часто у реальних завданнях нам потрібно:
- Отримати зі списку унікальні елементи (наприклад, список email → множина унікальних адрес).
- Побудувати мапу (Map), наприклад, зі списку імен отримати мапу «імʼя → довжина імені».
- Обʼєднати елементи у один рядок (наприклад, для красивого виведення).
Раніше для цього доводилося писати багато коду з циклами, умовами та тимчасовими колекціями. Зі Stream API усе стало простіше й… стильніше!
Приклад: отримати множину унікальних імен зі списку
Припустімо, у нас є список імен (раптом хтось у вашій програмі додав себе двічі — таке буває!):
List<String> names = List.of("Анна", "Сергій", "Анна", "Марія", "Іван", "Сергій");
Наше завдання — отримати колекцію, де кожне імʼя трапляється лише один раз, тобто множину (Set). За допомогою Stream API це робиться буквально в один рядок:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Виведення:
[Марія, Іван, Анна, Сергій]
(Порядок у Set не гарантується — не дивуйтеся, якщо у вас буде інший порядок.)
А якщо потрібно навпаки: Set → List?
Іноді потрібно навпаки — перетворити множину на список (наприклад, щоб відсортувати або отримати доступ за індексом):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Перетворення в Map: Collectors.toMap()
Приклад: зі списку імен отримати Map «імʼя → довжина імені»
Іноді хочеться бути не просто програмістом, а справжнім картографом — будувати мапи! Спробуймо:
List<String> names = List.of("Анна", "Сергій", "Марія", "Іван");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // ключ — саме імʼя
name -> name.length() // значення — довжина імені
));
System.out.println(nameToLength);
Виведення:
{Марія=5, Іван=4, Анна=4, Сергій=6}
Важливий момент: дублікати ключів
Якщо в початковому списку є однакові імена, то під час спроби зібрати їх у Map виникне помилка IllegalStateException: Duplicate key. Java не любить, коли ви намагаєтеся зберігати два значення за тим самим ключем.
Як обробити дублікати?
Можна вказати, що робити у разі збігу ключів — наприклад, залишити перше значення або останнє:
List<String> names = List.of("Анна", "Сергій", "Анна", "Марія", "Іван", "Сергій");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // залишити перше значення
));
System.out.println(nameToLength);
Тепер програма не завершиться з помилкою, і в Map потрапить лише перше входження кожного імені.
Приклад: Map з обʼєктами
Трохи ускладнимо: у нас є список користувачів, і ми хочемо побудувати Map «імʼя → користувач»:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Приклад списку користувачів
List<User> users = List.of(
new User("Анна", 25),
new User("Сергій", 30),
new User("Марія", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Виведення:
{Марія=Марія (22), Анна=Анна (25), Сергій=Сергій (30)}
3. Збирання у рядок: Collectors.joining()
Іноді хочеться не просто зібрати колекцію, а зробити красивий рядок для виведення користувачеві або у лог. Наприклад, зібрати всі імена через кому:
List<String> names = List.of("Анна", "Сергій", "Марія", "Іван");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Виведення:
Анна, Сергій, Марія, Іван
Можна додати префікс і суфікс
String result = names.stream()
.collect(Collectors.joining(", ", "Список: [", "]"));
System.out.println(result);
Виведення:
Список: [Анна, Сергій, Марія, Іван]
4. Термінальні операції: forEach, collect, count, anyMatch, allMatch, noneMatch
Метод forEach
З forEach ми вже добре знайомі: цей метод виконує дію для кожного елемента потоку.
names.stream().forEach(name -> System.out.println("Привіт, " + name + "!"));
Метод collect
Цей метод збирає елементи в колекцію, рядок або іншу структуру. Найчастіша операція — збирання у List або Set за допомогою Collectors.toList() і Collectors.toSet().
Метод count
Підраховує кількість елементів у потоці.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Імен довших за 4 літери: " + count);
Методи anyMatch, allMatch, noneMatch
Перевіряють, чи виконується умова щонайменше для одного елемента (anyMatch), для всіх (allMatch) або ні для жодного (noneMatch).
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("Є коротке імʼя? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("Усі імена довші за 3 літери? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Іван"));
System.out.println("Чи немає Івана? " + noneIvan);
Виведення:
Є коротке імʼя? false
Усі імена довші за 3 літери? true
Чи немає Івана? false
5. Термінальні й проміжні операції: закріпімо поняття
Проміжні операції (filter, map, distinct, sorted, limit, skip, peek) — повертають новий Stream, можна будувати ланцюжки.
Термінальні операції (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — завершують потік; після цього результатів більше не буде!
Приклад ланцюжка:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Виведення:
[АННА, ІВАН, МАРІЯ, СЕРГІЙ]
6. Типові помилки під час перетворення колекцій через Stream
Помилка № 1: Необроблені дублікати ключів у toMap
Якщо в початковій колекції трапляються дубльовані ключі, а ви використовуєте Collectors.toMap() без явної функції злиття, програма викине виняток. Для таких випадків завжди вказуйте функцію злиття:
// Залишити останнє значення
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Помилка № 2: Використання forEach замість collect
Іноді новачки намагаються «зібрати» колекцію за допомогою forEach, наприклад:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Працює, але це не шлях Stream!
Краще використовувати collect(Collectors.toList()) — це безпечніше й чистіше.
Помилка № 3: Спроба повторно використати потік
Потік можна використати лише один раз. Після термінальної операції (наприклад, collect, forEach) спроба продовжити працювати з цим самим Stream призведе до IllegalStateException.
Помилка № 4: Порушення принципу «без побічних ефектів»
Проміжні операції мають бути «чистими» (без зміни стану зовнішніх змінних). Не варто всередині map або filter щось змінювати поза потоком.
Помилка № 5: Не враховано порядок у Set і Map
Якщо важливий порядок елементів, використовуйте відповідні колекції — наприклад, LinkedHashSet, TreeMap — і вказуйте потрібний колектор.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ