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() могут нарушить детерминированный порядок вывода.

1
Задача
JAVA 25 SELF, 32 уровень, 1 лекция
Недоступна
Сбор урожая для грандиозного фруктового салата 🍎🍊
Сбор урожая для грандиозного фруктового салата 🍎🍊
1
Задача
JAVA 25 SELF, 32 уровень, 1 лекция
Недоступна
Оформление витрины с космическими объектами 🚀
Оформление витрины с космическими объектами 🚀
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ