Definition
- Intersection:
a.stream().filter(setB::contains).distinct().toList()keeps elements in both lists. - Union:
Stream.concat(a.stream(), b.stream()).distinct().toList()gives every element in either list. - HashSet for O(1) membership checks makes intersection O(m+n) instead of O(m*n).
- equals/hashCode must be correct on the element type for both operations.
- distinct() removes duplicates from both the intersection and union results.
Priya at the National Bank of India compares two branches' customer ID lists. The intersection gives the shared customers who have accounts at both branches. The union gives the total unique customer base across both branches.
Understanding the Concepts
What does this question ask?
The interviewer wants set intersection and union using streams. This tests filter with set membership, Stream.concat, distinct, and the performance of the lookup collection.
How do you find the intersection?
package com.bank.service;
import java.util.*;
import java.util.stream.Collectors;
public class IntersectionUnion {
public static void main(String[] args) {
List<String> branch1 = List.of(
"C001", "C002", "C003", "C004", "C005");
List<String> branch2 = List.of(
"C003", "C004", "C005", "C006", "C007");
Set<String> setB = new HashSet<>(branch2);
List<String> intersection = branch1.stream()
.filter(setB::contains)
.distinct()
.toList();
System.out.println("Intersection: " + intersection);
}
}
Intersection: [C003, C004, C005]
How do you find the union?
import java.util.stream.Stream;
List<String> union = Stream.concat(
branch1.stream(), branch2.stream())
.distinct()
.toList();
System.out.println("Union: " + union);
Union: [C001, C002, C003, C004, C005, C006, C007]
Why use a HashSet for the membership test?
| Lookup collection | contains time | Intersection time |
|---|---|---|
| List (ArrayList) | O(n) per call | O(m * n) total |
| HashSet | O(1) per call | O(m + n) total |
// WRONG: quadratic with list.contains
List<String> slow = branch1.stream()
.filter(branch2::contains) // O(n) per element!
.toList();
// CORRECT: linear with HashSet.contains
Set<String> fast = new HashSet<>(branch2);
List<String> quick = branch1.stream()
.filter(fast::contains) // O(1) per element
.toList();
How do you find the difference (A minus B)?
Set<String> setB = new HashSet<>(branch2);
List<String> difference = branch1.stream()
.filter(id -> !setB.contains(id))
.toList();
System.out.println("Only in branch1: " + difference);
Only in branch1: [C001, C002]
How do you find the symmetric difference?
Set<String> setA = new HashSet<>(branch1);
Set<String> setBB = new HashSet<>(branch2);
List<String> symmetricDiff = Stream.concat(
branch1.stream().filter(id -> !setBB.contains(id)),
branch2.stream().filter(id -> !setA.contains(id)))
.toList();
System.out.println("Symmetric difference: " + symmetricDiff);
Symmetric difference: [C001, C002, C006, C007]
What are common mistakes?
Mistake: using list.contains instead of set.contains.
// WRONG: O(m * n) quadratic on large lists
branch1.stream().filter(branch2::contains).toList();
// CORRECT: O(m + n) with HashSet
Set<String> setB = new HashSet<>(branch2);
branch1.stream().filter(setB::contains).toList();
Mistake: forgetting that equals/hashCode must be correct.
// If elements are custom objects without equals/hashCode,
// HashSet.contains uses Object identity, not value equality.
// Always override equals and hashCode for value types.
What about preserving order?
The intersection preserves the order of the first list. The union preserves encounter order because distinct keeps the first occurrence.
What are common interview follow-up questions?
- "How do you count the intersection size?" Replace toList with count().
- "How do you find common elements across three lists?" Chain two intersections: intersect(a, b) then intersect(result, c).
- "How do you find the Jaccard similarity?" intersection.size() / union.size().
Visual Overview
Branch 1: [C001, C002, C003, C004, C005] Branch 2: [C003, C004, C005, C006, C007] Intersection: filter(setB::contains) ┌──────────────────────────────────────┐ │ C001 in setB? NO → skip │ │ C002 in setB? NO → skip │ │ C003 in setB? YES → keep │ │ C004 in setB? YES → keep │ │ C005 in setB? YES → keep │ └──────────────────────────────────────┘ Result: [C003, C004, C005] Union: concat + distinct [C001,C002,C003,C004,C005,C003,C004,C005,C006,C007] → distinct → [C001,C002,C003,C004,C005,C006,C007]
Key Takeaway
Intersection filters one list by membership in a HashSet of the other, and union concatenates both streams and applies distinct. The HashSet is the single most important performance detail: filtering with a raw list's contains is quadratic, while a HashSet gives constant-time lookups.
Both operations depend on the element type having correct equals and hashCode. The intersection preserves the order of the first list, and the union preserves encounter order. At the National Bank of India, Rajesh used exactly this to compare two branches' customer ID lists for shared accounts and total unique customers.
The same pattern extends to difference (A minus B) by negating the filter, and to symmetric difference by combining both one-sided differences.
Remember one sentence: always build a HashSet for the membership test in intersection, because list.contains is O(n) per call while HashSet.contains is O(1).
---