1. Stream.concat: merging two streams
In programming, a common situation arises: we have two (or more) data streams and want to merge them into one. For example, two lists of students from different groups — and we need to process them all at once. In the “old days” (before the Stream API) we would simply merge two lists using addAll. But if we are working with streams (Stream<T>), we want to do this lazily and expressively.
Syntax and how it works
The most basic way to merge two streams is to use the static method Stream.concat:
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
What happens:
- It consumes the first stream first, then the second.
- The result is a new stream that first yields all elements from the first, then all elements from the second.
- The concatenation is lazy: until you start traversing the resulting stream with a terminal operation (for example, forEach or collect), nothing happens.
Example: merging two lists of names
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anna", "Boris", "Victoria");
List<String> groupB = List.of("Gregory", "Daria");
Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());
allStudents.forEach(System.out::println);
Result:
Anna
Boris
Victoria
Gregory
Daria
Important: after concatenation, the resulting stream is single-use. Like any other Stream, it cannot be reused.
Features of Stream.concat
- Only two streams at a time. For three or more — either chain concat calls or use other approaches (see below).
- Order is preserved. First the elements of the first stream, then the second.
- Lazy concatenation. If the first stream is infinite, execution will never reach the second.
- Common bug: trying to concatenate a stream with itself (Stream.concat(stream, stream)) causes the same stream to be reused — this is prohibited.
2. Merging more than two streams: flatMap and Stream.of
When there are more than two streams, it’s more convenient to gather them using the combination of Stream.of + flatMap.
Using Stream.of + flatMap
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anna", "Boris");
List<String> groupB = List.of("Victoria");
List<String> groupC = List.of("Gregory", "Daria");
Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
.flatMap(Collection::stream);
allStudents.forEach(System.out::println);
What happens here:
- Stream.of(groupA, groupB, groupC) creates a stream of three collections.
- flatMap(Collection::stream) flattens each collection into a single stream of strings.
- The result is one big stream of all students.
Advantage: you can merge as many streams as you like.
Another option: merging a list of streams
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);
Result:
a
b
c
d
e
3. Collectors.joining: join strings with a delimiter
Sometimes you don’t just need to merge streams, but to glue all elements into a single string — for example, to print names separated by commas. For this, there is the collector Collectors.joining.
Syntax
String result = stream.collect(Collectors.joining(", "));
- No arguments: joins strings without a delimiter.
- With a delimiter: the specified string will appear between elements.
- With prefix and suffix: Collectors.joining(delimiter, prefix, suffix)
Example: join a student list into one string
import java.util.List;
import java.util.stream.Collectors;
List<String> students = List.of("Anna", "Boris", "Victoria");
String line = students.stream()
.collect(Collectors.joining(", "));
System.out.println("Student list: " + line);
Result:
Student list: Anna, Boris, Victoria
Example with prefix and suffix
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
Result:
[Anna, Boris, Victoria]
Why is this useful?
- Forming CSV lines.
- Pretty printing of lists and reports.
- Passing data as a single string (for example, in a URL or a log).
4. Comparing concat and flatMap: when to use which?
- Stream.concat — when you have exactly two streams and order matters.
- Stream.of + flatMap — when you have many streams or they are stored in a collection.
- Collectors.joining — when elements are of type String and you need a single string as the result.
Comparison table
| Method | When to use | Code example |
|---|---|---|
|
Two streams | |
|
Many streams, a collection of streams | |
|
Join elements into one string | |
5. Practical examples in a learning app
Example 1. Merge students from two departments
import java.util.List;
import java.util.stream.Stream;
List<String> itStudents = List.of("Anna", "Boris");
List<String> mathStudents = List.of("Victoria", "Gregory");
Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());
all.forEach(System.out::println);
Example 2. Merge students from all groups and output as a single string
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
List<List<String>> allGroups = List.of(
List.of("Anna", "Boris"),
List.of("Victoria"),
List.of("Gregory", "Daria")
);
String allNames = allGroups.stream()
.flatMap(Collection::stream)
.collect(Collectors.joining("; "));
System.out.println("All students: " + allNames);
Result:
All students: Anna; Boris; Victoria; Gregory; Daria
Example 3. Merge streams of numbers
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. Important nuances and caveats
concat works only with two streams
For three streams you end up with nested calls — it works but looks noisy. It’s better to use 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);
Streams are single-use
After a terminal operation, a stream cannot be used. Reusing it results in IllegalStateException.
The concat-produced stream is lazy
If the first stream is infinite (for example, created via Stream.generate(...)), execution will never reach the second.
Element order
Order is always preserved: first the first stream, then the second.
Collectors.joining works only with Stream<String>
For non-strings, convert elements to strings first, for example via 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
Comparison with the “manual” approach
Before the Stream API:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
With the Stream API:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
Benefit of streams: you can insert intermediate operations (filter, map, and others) before and after concatenation, and you can work with arbitrary data sources.
7. Common mistakes when merging streams
Mistake #1: reusing a stream. A stream can be used only once. You must not concatenate a stream with itself.
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Error! s will be reused
Mistake #2: attempting to merge an infinite stream with a finite one. If the first stream is infinite, the second will never start.
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 will never be included in the result
Mistake #3: joining for non-strings. The Collectors.joining collector expects a stream of strings. For numbers, convert to strings first:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Compilation error!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
Mistake #4: breaking order. If order matters, use concat or an ordered flatMap. Methods like unordered() can break the deterministic output order.
GO TO FULL VERSION