Definition
- Stream.concat(a, b) – returns a lazily concatenated stream: all elements of
ain encounter order, then all elements ofb. - Deduplication – uses
toMap(Employee::getId, identity, (first, second) -> first)or thedistinctByKeyfilter pattern. - Ordering – the result is ordered if both inputs are ordered.
- For 3+ streams – use
Stream.of(s1, s2, s3).flatMap(Function.identity())to avoid nested concat. - Lazy – concat does not copy elements; it chains the two underlying spliterators.
Priya at the National Bank of India has two boxes of employee files: one from the Mumbai branch and one from the Delhi branch. Stream.concat empties the Mumbai box first, then the Delhi box, into a single line. Some employees transferred between branches and appear in both. She deduplicates by checking the employee ID stamped on each file.
Understanding the Concepts
What does this question ask?
The interviewer wants you to combine two streams and handle duplicates. This tests Stream.concat, deduplication patterns, and awareness of when to use concat vs flatMap.
How do you merge two lists with Stream.concat?
package com.bank.service;
import java.util.*;
import java.util.stream.*;
public class ConcatDemo {
public static void main(String[] args) {
List<Employee> mumbai = List.of(
new Employee("E001", "Rajesh", "TRADING"),
new Employee("E002", "Priya", "RISK"),
new Employee("E003", "Amit", "OPERATIONS"));
List<Employee> delhi = List.of(
new Employee("E002", "Priya", "RISK"),
new Employee("E004", "Kavita", "COMPLIANCE"),
new Employee("E005", "Suresh", "OPERATIONS"));
// Concat: all Mumbai first, then all Delhi
List<Employee> merged = Stream.concat(
mumbai.stream(), delhi.stream())
.toList();
System.out.println("Merged size: " + merged.size());
merged.forEach(e -> System.out.println(e.getId() + ": " + e.getName()));
}
}
Merged size: 6
E001: Rajesh
E002: Priya
E003: Amit
E002: Priya
E004: Kavita
E005: Suresh
E002 (Priya) appears twice because concat does not deduplicate.
How do you deduplicate after concat?
// Method 1: toMap with merge function
Map<String, Employee> dedupMap = Stream.concat(
mumbai.stream(), delhi.stream())
.collect(Collectors.toMap(
Employee::getId,
e -> e,
(first, second) -> first)); // keep Mumbai (first) on conflict
List<Employee> deduped = new ArrayList<>(dedupMap.values());
// Method 2: distinctByKey filter
List<Employee> deduped2 = Stream.concat(
mumbai.stream(), delhi.stream())
.filter(distinctByKey(Employee::getId))
.toList();
How is concat different from flatMap for combining streams?
| Feature | Stream.concat(a, b) | flatMap(identity) | |---|---|---| | Number of streams | Exactly 2 | Any number | | Syntax | Stream.concat(s1, s2) | Stream.of(s1, s2, s3).flatMap(s -> s) | | Nesting | Deeply nested for 3+ streams | Flat always | | Performance | Lopsided tree degrades for many levels | Flat and efficient | | Best for | Combining exactly 2 streams | Combining 3+ streams |
// Two streams: concat is clear
Stream<Employee> two = Stream.concat(mumbai.stream(), delhi.stream());
// Three or more: flatMap is better
List<Employee> pune = List.of(new Employee("E006", "Neha", "IT"));
Stream<Employee> three = Stream.of(
mumbai.stream(), delhi.stream(), pune.stream())
.flatMap(Function.identity());
// WRONG: deeply nested concat for many streams
Stream<Employee> nested = Stream.concat(
Stream.concat(mumbai.stream(), delhi.stream()),
pune.stream()); // works but builds lopsided tree
What are common mistakes?
Mistake 1: reusing streams after concat.
// WRONG: streams can only be consumed once
Stream<Employee> mumbaiStream = mumbai.stream();
Stream<Employee> merged1 = Stream.concat(mumbaiStream, delhi.stream());
merged1.forEach(System.out::println);
// This throws IllegalStateException: stream already operated on
Stream<Employee> merged2 = Stream.concat(mumbaiStream, delhi.stream());
Mistake 2: expecting concat to deduplicate automatically.
// WRONG assumption: concat does NOT remove duplicates
List<Employee> wrong = Stream.concat(mumbai.stream(), delhi.stream())
.toList(); // still has duplicate E002!
// CORRECT: explicitly deduplicate
List<Employee> correct = Stream.concat(mumbai.stream(), delhi.stream())
.filter(distinctByKey(Employee::getId))
.toList();
What about preserving order during dedup?
toMap with HashMap does not preserve insertion order. Use LinkedHashMap to maintain the concat order.
LinkedHashMap<String, Employee> ordered = Stream.concat(
mumbai.stream(), delhi.stream())
.collect(Collectors.toMap(
Employee::getId,
e -> e,
(first, second) -> first,
LinkedHashMap::new));
List<Employee> orderedList = new ArrayList<>(ordered.values());
// [E001, E002, E003, E004, E005] in concat order
What should a senior engineer know?
- concat creates a lazily concatenated spliterator. No elements are copied until the terminal operation consumes them.
- If either input stream is parallel, the resulting stream is parallel.
- For very large merges, consider database-level UNION instead of in-memory concat.
- The distinctByKey pattern with ConcurrentHashMap is parallel-safe; the toMap approach with a merge function is also parallel-safe.
What are common interview follow-up questions?
- "How do you merge and sort?" Chain sorted() after concat:
Stream.concat(a, b).sorted(comparing(salary)). - "How do you merge with priority (take newer record)?" Use a merge function that checks a timestamp field.
- "What is the time complexity?" O(n + m) for concat plus O(n + m) for dedup, so O(n + m) overall.
Visual Overview
Mumbai stream Delhi stream
[E001, E002, E003] [E002, E004, E005]
│ │
└──────────┬───────────────┘
▼
Stream.concat()
[E001, E002, E003, E002, E004, E005]
│
│ toMap(id, e, keep-first)
▼
┌───────────────────────────────────┐
│ E001 → Rajesh (Mumbai) │
│ E002 → Priya (Mumbai, kept) │
│ E003 → Amit (Mumbai) │
│ E004 → Kavita (Delhi, new) │
│ E005 → Suresh (Delhi, new) │
└───────────────────────────────────┘
Key Takeaway
Stream.concat joins exactly two streams into one, emitting all elements of the first in order followed by all elements of the second. The result is lazy: no elements are copied until the terminal operation consumes them.
After concat, duplicates remain because concat does not deduplicate. Use toMap(getId, identity, mergeFunction) to collect into a Map where duplicate IDs are resolved by the merge function, or use the distinctByKey filter pattern for inline deduplication.
For three or more streams, avoid deeply nested concat calls which build a lopsided tree. Instead use Stream.of(s1, s2, s3).flatMap(Function.identity()) which keeps the combination flat and efficient.
Remember one sentence: concat joins two streams lazily in order but does not deduplicate, so follow it with toMap or distinctByKey to handle overlapping IDs.
---