JavaRush /Курси /JAVA 25 SELF /Базові операції Stream API: map, filter, collect

Базові операції Stream API: map, filter, collect

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

1. Створюємо потік

Щоб користуватися Stream API, спочатку потрібно отримати потік із колекції чи масиву.

Приклади створення потоку

// Зі списку
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();

// З масиву
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// З окремих значень
Stream<String> letters = Stream.of("A", "B", "C");

Стисло:

  • list.stream() — для колекцій
  • Arrays.stream(array) — для масивів
  • Stream.of(...) — для окремих значень

Приклад у контексті нашого застосунку

Припустімо, у нас є список користувачів:

List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();

Проміжні та термінальні операції

Важливий момент: операції Stream API поділяються на два типи.

  • Проміжні операції (наприклад, filter, map, distinct) — описують етапи обробки. Вони повертають новий потік, але самі по собі не запускають виконання.
  • Термінальні операції (наприклад, collect, forEach, count) — запускають конвеєр і повертають результат.

Потік працює «ледачо»: доки не буде викликано термінальну операцію, жодних обчислень не відбудеться. Саме тому ми часто завершуємо ланцюжок викликом collect(...) — це і є точка, де потік перетворюється назад на колекцію або інший результат.

2. Операція filter: фільтруємо елементи за умовою

filter — це проміжна операція, яка пропускає лише ті елементи, що відповідають заданій умові.

Сигнатура

Stream<T> filter(Predicate<? super T> predicate); 

Predicate — це функціональний інтерфейс, що приймає елемент і повертає true (залишити) або false (відсіяти).

Приклад: залишити лише парні числа

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

Що відбувається?

  • n -> n % 2 == 0 — лямбда-вираз, який перевіряє, чи ділиться число на 2 без остачі.
  • filter залишає лише парні числа.

Приклад: фільтруємо імена, що починаються з «A»

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(aNames); // [Anna, Alex, Alina]

Важливий момент: filter не змінює колекцію — він створює новий потік, у якому є лише потрібні елементи.

3. Операція map: перетворюємо елемент на щось інше

map — це операція перетворення. Вона бере кожен елемент потоку, застосовує до нього функцію й повертає нове значення.

Сигнатура

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Function — це інтерфейс, що приймає елемент і повертає значення (може бути іншого типу).

Приклад: отримати довжини рядків

List<String> names = List.of("Anna", "Boris", "Alex");

List<Integer> nameLengths = names.stream()
    .map(name -> name.length())
    .collect(Collectors.toList());

System.out.println(nameLengths); // [4, 5, 4]

Що відбувається?

  • map перетворює рядок на його довжину (name -> name.length()).
  • У результаті маємо потік чисел.

Приклад: перетворити рядки на верхній регістр

List<String> names = List.of("Anna", "Boris", "Alex");

List<String> upperNames = names.stream()
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

System.out.println(upperNames); // [ANNA, BORIS, ALEX]

4. Операція collect: збираємо результат назад у колекцію

collect — це термінальна операція, тобто вона завершує роботу потоку й збирає результат у колекцію або інший контейнер.

Сигнатура

<R, A> R collect(Collector<? super T, A, R> collector)

Не лякайтеся «страшної» сигнатури! У 99 % випадків ви використовуєте готові колектори з класу Collectors.

Collectors — це допоміжний клас із набором «збирачів». Він підказує потоку, у яку форму зібрати результат: список, множину, рядок тощо.

Приклади:

  • Collectors.toList() — у List
  • Collectors.toSet() — у Set
  • Collectors.joining(", ") — у рядок через кому

Тобто Collectors — це як набір коробок різної форми, у які ви пакуєте елементи потоку.

Приклад: зібрати результат у List

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());

Приклад: зібрати результат у Set

Set<String> uniqueNames = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

Приклад: зібрати рядки через кому

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result); // Anna, Boris, Alex

5. Ланцюжок операцій: фільтрація + перетворення + збирання результату

Найбільша сила Stream API — це можливість ланцюжити операції одну за одною.

Приклад: отримати довжини імен, що починаються з «A»

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<Integer> aNameLengths = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(aNameLengths); // [4, 4, 5]

Покроково:

  1. .stream() — створюємо потік зі списку.
  2. .filter(name -> name.startsWith("A")) — залишаємо тільки імена, що починаються на "A".
  3. .map(String::length) — перетворюємо кожне імʼя на його довжину.
  4. .collect(Collectors.toList()) — збираємо результат у список.

Аналогічний імперативний код

Ось як би це виглядало «по‑старому»:

List<Integer> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.length());
    }
}

Порівняйте: Stream API — одним рядком, читається як «що робимо», а не «як робимо».

6. Практика: кілька коротких завдань

Потренуймося! Усі приклади можна запускати в одному файлі — просто змінюйте дані.

Завдання 1: залишити лише непарні числа й звести їх у квадрат

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<Integer> oddSquares = numbers.stream()
    .filter(n -> n % 2 != 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

System.out.println(oddSquares); // [1, 9, 25, 49]

Завдання 2: зі списку рядків отримати список перших літер

List<String> names = List.of("Anna", "Boris", "Alex");

List<Character> initials = names.stream()
    .map(name -> name.charAt(0))
    .collect(Collectors.toList());

System.out.println(initials); // [A, B, A]

Завдання 3: відфільтрувати рядки довжиною понад 3 та зібрати їх у Set

List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");

Set<String> longWords = words.stream()
    .filter(word -> word.length() > 3)
    .collect(Collectors.toSet());

System.out.println(longWords); // [bear, elephant]

7. Типові помилки під час роботи з filter, map, collect

Помилка № 1: забули collect — результату немає!
Stream API ледачий, як кіт на підвіконні: доки не викличете термінальну операцію (наприклад, collect або forEach), нічого не відбудеться. Якщо написати лише users.stream().filter(...).map(...); — це не виконає жодних дій.

Помилка № 2: filter і map сплутані місцями
Іноді новачки спочатку роблять map, а потім filter. Наприклад, names.stream().map(String::length).filter(len -> len > 3) — це поверне числа, а не рядки. Якщо вам потрібні рядки довжиною понад 3, спершу фільтруйте, потім перетворюйте.

Помилка № 3: забули про незмінність
Операції Stream API не змінюють вихідну колекцію! Вони повертають новий результат. Після List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — колекція names залишиться незмінною.

Помилка № 4: спроба використовувати зовнішній змінюваний список
Не варто робити так:

List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));

Краще використовувати collect — це безпечніше й коротше.

Помилка № 5: NullPointerException
Якщо в колекції можуть бути null-елементи, то виклик name.startsWith("A") на null призведе до помилки. Додавайте перевірку на null, якщо це можливо:

.filter(name -> name != null && name.startsWith("A"))
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ