Definition
- concat(a, b) – lazily concatenates exactly two streams; elements from a first, then b.
- flatMap(identity) –
Stream.of(s1, s2, s3).flatMap(Function.identity())flattens a stream of streams; handles any number cleanly. - Nested concat problem –
concat(concat(a, b), c)builds a left-skewed tree; for many streams this causes deep pipeline nesting. - Both lazy – neither stream is consumed until a terminal operation triggers.
- Close propagation – concat closes both input streams; flatMap closes each sub-stream.
Connecting two conveyor belts: concat clips them end-to-end (simple, efficient). Connecting 10 belts: concat means clipping belt1 to belt2, then that to belt3, and so on. flatMap puts all belts in a box and lays them end-to-end in one step.
Understanding the Concepts
When do you use concat vs flatMap?
import java.util.stream.Stream;
import java.util.function.Function;
// Two streams: concat is cleaner
Stream<Trade> two = Stream.concat(
exchangeTrades.stream(),
internalTrades.stream());
// Three or more streams: flatMap is cleaner
Stream<Trade> three = Stream.of(
exchangeTrades.stream(),
internalTrades.stream(),
manualTrades.stream()
).flatMap(Function.identity());
Why is nested concat a problem?
// WRONG: deeply nested concat for many streams
Stream<Trade> nested = Stream.concat(
Stream.concat(
Stream.concat(exchangeTrades.stream(), internalTrades.stream()),
manualTrades.stream()),
otcTrades.stream());
// Deep nesting, hard to read, potential stack issues for many streams
// CORRECT: flatMap is flat and readable for N streams
Stream<Trade> flat = Stream.of(
exchangeTrades.stream(),
internalTrades.stream(),
manualTrades.stream(),
otcTrades.stream()
).flatMap(Function.identity());
Nested concat builds a left-skewed pipeline tree that can cause deep recursion for many streams.
What are the trade-offs?
| Aspect | concat(a, b) | flatMap(identity) |
|---|---|---|
| Best for | 2 streams | 3+ streams |
| Nesting | Clean for 2 | Clean for N |
| Overhead | Minimal | Slightly more per element |
| Close | Propagates to both | Propagates to each sub-stream |
| Lazy | Yes | Yes |
| Order | Preserved | Preserved |
How does concat handle resource streams?
concat propagates close() to both input streams, which is important for resource-backed streams like Files.lines.
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
// concat closes both file-backed streams
try (Stream<String> merged = Stream.concat(
Files.lines(Path.of("/bank/settled.csv")),
Files.lines(Path.of("/bank/pending.csv")))) {
List<String> all = merged.toList();
} // both file handles closed
How do you merge collections (not streams)?
import java.util.Collection;
import java.util.List;
// Use Collection::stream when merging collections
List<String> all = Stream.of(listA, listB, listC)
.flatMap(Collection::stream)
.toList();
// Cleaner than: Stream.of(listA.stream(), listB.stream(), listC.stream()).flatMap(identity)
What about parallel stream behavior with concat?
// concat: the result stream's parallel mode follows the first stream
Stream<String> parallel = Stream.concat(
list1.parallelStream(), // parallel
list2.stream()); // sequential
// Result is parallel (follows first)
// flatMap: follows the outer stream's mode
Stream<String> parallel = Stream.of(list1, list2)
.parallel()
.flatMap(Collection::stream);
What are common mistakes?
// WRONG: nesting concat for 4+ streams
Stream.concat(Stream.concat(Stream.concat(a, b), c), d); // unreadable!
// CORRECT: flatMap for 3+ streams
Stream.of(a, b, c, d).flatMap(Function.identity());
// WRONG: forgetting to close resource streams after concat
Stream<String> merged = Stream.concat(Files.lines(path1), Files.lines(path2));
merged.toList(); // handles leaked!
// CORRECT: try-with-resources
try (Stream<String> merged = Stream.concat(Files.lines(path1), Files.lines(path2))) {
merged.toList();
}
What should a senior engineer know?
- For very many streams (100+), flatMap(identity) is significantly better than nested concat.
- Both work with parallel streams, but concat creates a balanced split only if both inputs are splittable.
- Use
Stream.of(list1, list2, list3).flatMap(Collection::stream)when merging collections, not streams. - concat supports exactly two streams. For more, use flatMap or a loop building a stream.
- Both approaches are lazy: neither source stream is consumed until a terminal operation triggers.
Visual Overview
Two streams → concat: Stream A: [1, 2, 3] Stream B: [4, 5, 6] concat: [1, 2, 3, 4, 5, 6] Three+ streams → flatMap: Stream.of(A, B, C).flatMap(identity): [1, 2, 3, 4, 5, 6, 7, 8, 9] Flat, clean, no nesting.
Key Takeaway
For two streams, Stream.concat(a, b) is clean and efficient. For three or more, Stream.of(s1, s2, s3).flatMap(Function.identity()) avoids deeply nested concat calls that build an unbalanced pipeline tree.
Both approaches are lazy and preserve encounter order. concat propagates close() to both inputs, important for resource-backed streams. flatMap has slightly more per-element overhead but scales cleanly to any number of sources.
Remember one sentence: use concat for two streams and flatMap(identity) for three or more to keep the pipeline flat and readable.
---