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 и нужен результат в виде одной строки.
Таблица сравнения
| Метод | Когда использовать | Пример кода |
|---|---|---|
|
Два потока | |
|
Много потоков, коллекция потоков | |
|
Склеить элементы в одну строку | |
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() могут нарушить детерминированный порядок вывода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ