JavaRush /Курси /JAVA 25 SELF /Перетворення колекцій через Stream

Перетворення колекцій через Stream

JAVA 25 SELF
Рівень 30 , Лекція 4
Відкрита

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 — і вказуйте потрібний колектор.

1
Опитування
Основи Stream API, рівень 30, лекція 4
Недоступний
Основи Stream API
Основи Stream API
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ