JavaRush /Курси /JAVA 25 SELF /Методи join, concat: обʼєднання потоків

Методи join, concat: обʼєднання потоків

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

1. Stream.concat: обʼєднуємо два потоки

У програмуванні часто виникає ситуація: маємо два (або більше) потоки даних і хочемо обʼєднати їх в один. Наприклад, два списки студентів із різних груп — і потрібно обробити всіх одразу. У «старі часи» (до Stream API) ми б просто обʼєднали два списки за допомогою addAll. Але якщо ми працюємо з потоками (Stream<T>), хочеться зробити це лаконічно та виразно.

Синтаксис і принцип роботи

Найпростіший спосіб обʼєднати два потоки — використати статичний метод Stream.concat:

Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)

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

  • Береться спочатку перший потік, потім другий.
  • На виході — новий потік, який спочатку віддає всі елементи з першого, потім усі елементи з другого.
  • Обʼєднання ледаче: доки не почнете перебирати підсумковий потік термінальною операцією (наприклад, forEach або collect), нічого не відбудеться.

Приклад: обʼєднання двох списків імен

import java.util.List;
import java.util.stream.Stream;

List<String> groupA = List.of("Аня", "Борис", "Віка");
List<String> groupB = List.of("Гріша", "Даша");

Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());

allStudents.forEach(System.out::println);

Результат:

Аня
Борис
Віка
Гріша
Даша

Важливо: після обʼєднання підсумковий потік одноразовий. Як і будь-який інший Stream, його не можна використати повторно.

Особливості Stream.concat

  • Лише два потоки водночас. Для трьох і більше — або ланцюжком concat, або інші підходи (див. нижче).
  • Порядок зберігається. Спочатку елементи першого потоку, потім другого.
  • Ледаче обʼєднання. Якщо перший потік нескінченний, до другого виконання ніколи не дійде.
  • Типова помилка: спроба обʼєднати потік сам із собою (Stream.concat(stream, stream)) призводить до повторного використання одного й того самого потоку — це заборонено.

2. Обʼєднання більш ніж двох потоків: flatMap і Stream.of

Коли потоків більше двох, зручніше збирати їх за допомогою комбінації Stream.of + flatMap.

Використовуємо Stream.of + flatMap

import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

List<String> groupA = List.of("Аня", "Борис");
List<String> groupB = List.of("Віка");
List<String> groupC = List.of("Гріша", "Даша");

Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
    .flatMap(Collection::stream);

allStudents.forEach(System.out::println);

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

  • Stream.of(groupA, groupB, groupC) створює потік із трьох колекцій.
  • flatMap(Collection::stream) розгортає кожну колекцію в єдиний потік рядків.
  • Підсумок — один великий потік усіх студентів.

Перевага: можна обʼєднувати скільки завгодно потоків.

Ще варіант: обʼєднання списку потоків

import java.util.List;
import java.util.stream.Stream;

List<Stream<String>> streams = List.of(
    Stream.of("a", "b"),
    Stream.of("c"),
    Stream.of("d", "e")
);

Stream<String> merged = streams.stream()
    .flatMap(s -> s);

merged.forEach(System.out::println);

Результат:

a
b
c
d
e

3. Collectors.joining: обʼєднуємо рядки з роздільником

Іноді потрібно не просто обʼєднати потоки, а склеїти всі елементи в один рядок — наприклад, щоб вивести імена через кому. Для цього є колектор Collectors.joining.

Синтаксис

String result = stream.collect(Collectors.joining(", "));
  • Без аргументів: обʼєднає рядки без роздільника.
  • З роздільником: між елементами буде вказаний рядок.
  • З префіксом і суфіксом: Collectors.joining(delimiter, prefix, suffix)

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

import java.util.List;
import java.util.stream.Collectors;

List<String> students = List.of("Аня", "Борис", "Віка");

String line = students.stream()
    .collect(Collectors.joining(", "));

System.out.println("Список студентів: " + line);

Результат:

Список студентів: Аня, Борис, Віка

Приклад із префіксом і суфіксом

String line = students.stream()
    .collect(Collectors.joining(", ", "[", "]"));

System.out.println(line);

Результат:

[Аня, Борис, Віка]

Навіщо це потрібно?

  • Формування CSV-рядків.
  • Зручний друк списків і звітів.
  • Передача даних в один рядок (наприклад, у URL або журнал).

4. Порівняння concat і flatMap: коли що використовувати?

  • Stream.concat — коли є рівно два потоки й важливо зберегти порядок.
  • Stream.of + flatMap — коли потоків багато або вони зберігаються в колекції.
  • Collectors.joining — коли елементи типу String і потрібен результат у вигляді одного рядка.

Таблиця порівняння

Метод Коли використовувати Приклад коду
Stream.concat(a, b)
Два потоки
Stream.concat(a, b)
Stream.of(...).flatMap()
Багато потоків, колекція потоків
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
Склеїти елементи в один рядок
stream.collect(Collectors.joining(", "))

5. Практичні приклади в навчальному застосунку

Приклад 1. Обʼєднуємо студентів із двох факультетів

import java.util.List;
import java.util.stream.Stream;

List<String> itStudents = List.of("Аня", "Борис");
List<String> mathStudents = List.of("Віка", "Гріша");

Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());

all.forEach(System.out::println);

Приклад 2. Обʼєднуємо студентів із усіх груп, виводимо в один рядок

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

List<List<String>> allGroups = List.of(
    List.of("Аня", "Борис"),
    List.of("Віка"),
    List.of("Гріша", "Даша")
);

String allNames = allGroups.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.joining("; "));

System.out.println("Усі студенти: " + allNames);

Результат:

Усі студенти: Аня; Борис; Віка; Гріша; Даша

Приклад 3. Обʼєднуємо потоки чисел

import java.util.stream.Stream;

Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);

Stream<Integer> merged = Stream.concat(s1, s2);

merged.forEach(System.out::print); // 12345

6. Важливі нюанси та особливості

concat працює лише з двома потоками

Для трьох потоків вийде вкладений запис — це працює, але виглядає громіздко. Краще використовувати Stream.of + flatMap:

Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);

Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);

Потоки одноразові

Після термінальної операції потік повторно використовувати не можна. Повторне використання призводить до IllegalStateException.

Потік concat ледачий

Якщо перший потік нескінченний (наприклад, створений через Stream.generate(...)), до другого виконання ніколи не дійде.

Порядок елементів

Порядок завжди зберігається: спочатку перший потік, потім другий.

Collectors.joining працює лише з Stream<String>

Для нерядкових елементів спочатку перетворіть їх на рядки, наприклад, через map(Object::toString):

import java.util.stream.Collectors;
import java.util.stream.Stream;

Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
    .map(Object::toString)
    .collect(Collectors.joining(", "));

System.out.println(line); // 1, 2, 3

Порівняння з «ручним» підходом

До Stream API:

import java.util.ArrayList;
import java.util.List;

List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);

З Stream API:

import java.util.stream.Collectors;

List<String> merged = Stream.concat(listA.stream(), listB.stream())
    .collect(Collectors.toList());

Плюс потоків: можна підключати проміжні операції (filter, map та ін.) до і після обʼєднання, а також працювати з будь-якими джерелами даних.

7. Типові помилки при обʼєднанні потоків

Помилка №1: повторне використання потоку. Потік можна використовувати лише один раз. Не можна обʼєднувати потік сам із собою.

import java.util.stream.Stream;

Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Помилка! s буде використано повторно

Помилка №2: спроба обʼєднати нескінченний потік зі скінченним. Якщо перший потік нескінченний, другий ніколи не почнеться.

import java.util.stream.Stream;

Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);

Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // finite не потрапить до результату

Помилка №3: joining для нерядкових елементів. Колектор Collectors.joining приймає потік рядків. Для чисел потрібно попередньо перетворити на рядки:

import java.util.List;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Помилка компіляції!

String joined = numbers.stream()
    .map(Object::toString)
    .collect(Collectors.joining(", "));

Помилка №4: порушення порядку. Якщо порядок важливий, використовуйте concat або впорядкований flatMap. Методи на кшталт unordered() можуть порушити детермінований порядок виведення.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ