Definition
- **Stream.concat(list1.stream(), list2.stream())** joins two streams into one sequential stream. - **Collectors.toMap(keyMapper, valueMapper, mergeFunction)** collects into a Map where the merge function handles duplicate keys. - **Keep-existing**: `(existing, incoming) -> existing` retains the first occurrence when IDs clash. - **Keep-incoming**: `(existing, incoming) -> incoming` retains the last occurrence (latest update). - **Deep-merge**: `(existing, incoming) -> new Employee(existing.id(), incoming.name(), existing.department(), incoming.salary())` selects specific fields from each source. - Without the merge function, duplicate keys throw `IllegalStateException`.
National Bank of India has two branches that each maintain their own employee lists. Some employees transferred between branches and appear in both lists. The head office merges the lists. "Keep-existing" means the head office record wins. "Keep-incoming" means the branch's updated record wins. "Deep-merge" means take the latest salary from the branch but keep the department assignment from head office.
Understanding the Concepts
What is the problem?
Two lists of employees may contain overlapping entries (same employee ID). You need to merge them into a single list where each employee ID appears exactly once, with a strategy for resolving duplicates.
How does the keep-existing strategy work?
The first list's entry wins when duplicate IDs are found.
package com.bank.service;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.*;
public class EmployeeMergeDemo {
record Employee(String id, String name, String department,
BigDecimal salary) {}
public static void main(String[] args) {
List<Employee> hqList = List.of(
new Employee("E001", "Rajesh", "TRADING", new BigDecimal("1800000")),
new Employee("E002", "Priya", "RISK", new BigDecimal("1000000")),
new Employee("E003", "Amit", "OPERATIONS", new BigDecimal("1500000"))
);
List<Employee> branchList = List.of(
new Employee("E002", "Priya", "RISK", new BigDecimal("1100000")),
new Employee("E003", "Amit", "TRADING", new BigDecimal("1600000")),
new Employee("E004", "Kavita", "RISK", new BigDecimal("1200000"))
);
// KEEP-EXISTING: HQ record wins on duplicate IDs
Map<String, Employee> merged = Stream.concat(
hqList.stream(), branchList.stream())
.collect(Collectors.toMap(
Employee::id,
e -> e,
(existing, incoming) -> existing)); // keep first
merged.values().forEach(e ->
System.out.println(e.id() + ": " + e.name()
+ " " + e.department() + " Rs." + e.salary()));
}
}
E001: Rajesh TRADING Rs.1800000
E002: Priya RISK Rs.1000000
E003: Amit OPERATIONS Rs.1500000
E004: Kavita RISK Rs.1200000
E002 and E003 use the HQ values because the keep-existing strategy retains the first occurrence.
How does the keep-incoming strategy work?
// KEEP-INCOMING: branch's updated record wins
Map<String, Employee> keepIncoming = Stream.concat(
hqList.stream(), branchList.stream())
.collect(Collectors.toMap(
Employee::id,
e -> e,
(existing, incoming) -> incoming)); // keep last
// E002: Priya RISK Rs.1100000 (branch salary)
// E003: Amit TRADING Rs.1600000 (branch dept + salary)
How does deep-merge work?
Select specific fields from each source to build the merged employee.
// DEEP-MERGE: keep HQ department, take branch salary
Map<String, Employee> deepMerged = Stream.concat(
hqList.stream(), branchList.stream())
.collect(Collectors.toMap(
Employee::id,
e -> e,
(hq, branch) -> new Employee(
hq.id(),
branch.name(), // take updated name
hq.department(), // keep HQ department
branch.salary()))); // take updated salary
// E003: Amit OPERATIONS Rs.1600000 (HQ dept + branch salary)
What happens without a merge function?
// WRONG: no merge function -- throws on duplicate keys
Map<String, Employee> broken = Stream.concat(
hqList.stream(), branchList.stream())
.collect(Collectors.toMap(
Employee::id,
e -> e)); // IllegalStateException: Duplicate key E002
How do you convert the result back to a sorted list?
// Convert Map values to sorted List
List<Employee> mergedList = merged.values().stream()
.sorted(Comparator.comparing(Employee::id))
.toList();
How is toMap different from groupingBy for merging?
| Collector | Duplicate Handling | Result Type |
|-----------|-------------------|-------------|
| toMap with merge | Merge function decides which value wins | Map |
| groupingBy | Keeps ALL values in a list | Map |
Use toMap when you want exactly one value per key. Use groupingBy when you want all values per key.
What are common mistakes?
WRONG: Forgetting the merge function and getting IllegalStateException.
WRONG: Assuming Stream.concat removes duplicates. It does not. It simply concatenates the two streams.
What should a senior engineer know?
- The merge function must be associative for parallel streams.
(a, b) -> aand(a, b) -> bare both associative. - For large lists, consider using
toMapwith aLinkedHashMap::newsupplier (four-argumenttoMap) to preserve insertion order. - In a real banking merge, the deep-merge strategy often needs validation: for example, ensure the branch salary is not lower than the HQ salary before accepting it.
Stream.concatcreates a lazily concatenated stream, so it does not copy all elements upfront.
Visual Overview
HQ List Branch List
┌────────────────┐ ┌────────────────┐
│ E001 Rajesh │ │ E002 Priya * │
│ E002 Priya │ │ E003 Amit * │
│ E003 Amit │ │ E004 Kavita │
└───────┬────────┘ └───────┬────────┘
│ │
└─────────┬───────────────┘
▼
Stream.concat()
[E001, E002, E003, E002*, E003*, E004]
│
▼
toMap(id, merge)
┌──────────────────────┐
│ E001 → Rajesh (HQ) │
│ E002 → Priya (merged)│
│ E003 → Amit (merged) │
│ E004 → Kavita (new) │
└──────────────────────┘
Key Takeaway
Merging two employee lists with unique IDs uses Stream.concat to combine both streams, then Collectors.toMap with a merge function to handle duplicate keys. Without the merge function, duplicate keys throw IllegalStateException. The merge function (existing, incoming) -> existing keeps the first occurrence. (existing, incoming) -> incoming keeps the last. A custom merge function selects specific fields from each source for a deep-merge.
At National Bank of India, the head office merges its list with the branch list. E002 and E003 appear in both. The keep-existing strategy preserves HQ salary for E002 (Rs.10L). The keep-incoming strategy takes the branch's updated salary (Rs.11L). The deep-merge strategy takes the name and salary from the branch but keeps the department from HQ.
Remember one sentence: toMap with a merge function is the stream way to deduplicate by key, and the merge function decides which value survives when keys collide.
---