Stream coding problems
Twelve classic Java stream coding problems, grouped by collector and operation choice, with interview follow-ups and trade-offs.
On this page
- Picture it first: choose the family before the method
- Warm-up problems: ranking, counting and first unique values
- Problem 1: second-highest salary
- Problem 2: frequency of each word, sorted by count desc then alphabetically
- Problem 3: first non-repeating character
- Core collector problems: groups, duplicates and top totals
- Problem 4: group employees by department with average salary
- Problem 5: find duplicates in a list
- Problem 6: top 3 customers by total order value
- Reshaping problems: flatten, partition and choose a best value
- Problem 7: flatten orders into distinct product names sorted alphabetically
- Problem 8: partition numbers into primes and non-primes
- Problem 9: longest string per group
- Harder problems: when streams start to creak
- Problem 10: merge two sorted lists via streams, and why a loop is better
- Problem 11: department with the highest headcount
- Problem 12: running total, and why streams are awkward
- Advanced patterns and interview trade-offs
- How to say it in the interview
🧩 What are stream coding problems?
Stream coding problems are small interview tasks where the hard part is choosing the right stream shape, not memorising a method name. They act like a set of keys: groupingBy opens bucket problems, flatMap opens nested collections, and sorted plus limit opens ranking. A good answer says the pattern out loud before writing code.
For this lesson, we'll focus on 12 classic Java 21 stream problems, ordered from easy to hard. I will keep each solution compact, but I will also call out when a stream is not the best production choice. Interviewers ask these because they reveal whether you understand encounter order, downstream collectors, tie-breakers and the cost of sorting.
Below the line (out of scope)
- Pipeline execution and laziness: How a stream pipeline executes
- Filtering, mapping and flattening: filter, map and flatMap
- Sorting and distinct rules: Sorting, distinct and distinctBy
- Grouping and boolean splits: groupingBy and partitioningBy in depth
- Map collectors and duplicate keys: Collectors.toMap and working with maps
- Index recipes: Stream recipes: indices, last element and strings
The 30-second answer
I start by naming the problem family: rank, count, group, flatten, partition or scan. Ranking usually means sorted, distinct, skip or limit; counting and totals usually mean groupingBy with counting, summingInt or averagingInt; nested collections usually need flatMap; boolean splits use partitioningBy. If the task needs a running total or a true merge of two already sorted lists, I mention that a loop is clearer and often O(n) instead of sorting everything again. The stream solution can still be useful in an interview, but the trade-off has to be part of the answer.
What you’ll learn
- Solve salary, frequency and first-unique problems with
distinct,groupingBy,countingand stable ordering - Use
groupingBydownstream collectors for averages, totals, max values and headcounts - Reach for
flatMap,distinct,sortedandpartitioningBywhen the problem shape calls for them - Explain when a stream solution is readable and when a plain loop is the better interview answer
- Handle tie-breakers, empty inputs, duplicate keys and encounter-order requirements without hand-waving
- Talk through
IntStream.rangerecipes for index-aware work, including why running totals are awkward in streams
01Picture it first: choose the family before the method
Think of the prompt as a package on a sorting belt. If the package asks for "per department", it goes to the grouping lane. If it asks for "top 3", it goes to the ranking lane. If it asks for "all products inside all orders", it goes to the flattening lane.
That first lane choice matters more than the exact syntax. Most wrong stream answers start from a favorite method instead of the problem shape.
A map from stream interview problem families to the collectors and operations that usually solve them
Notice the split. Problems 1, 2, 6 and 11 care about ranking or aggregation. Problems 7 and 8 are shape changes. Problems 3 and 12 care about encounter order, where streams need extra care.
02Warm-up problems: ranking, counting and first unique values
These first three are common because they fit on a whiteboard and still expose real edges: duplicates, deterministic ordering and empty input.
03Problem 1: second-highest salary
Interviewer prompt: "Given a list of employees, return the second-highest distinct salary. If there is no second salary, fail clearly."
Try it yourself first
Before reading on, decide whether two employees with the same highest salary should count as first and second, or as one distinct salary.
import java.util.Comparator;
import java.util.List;
final class Problem1SecondHighestSalary {
record Employee(String name, String department, int salary) { }
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Mina", "Platform", 160_000),
new Employee("Omar", "Data", 150_000),
new Employee("Ravi", "Platform", 160_000),
new Employee("Iris", "Support", 120_000)
);
int secondHighest = employees.stream()
.map(Employee::salary)
.distinct()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst()
.orElseThrow();
System.out.println(secondHighest); // 150000
}
}
Why this works: map narrows the stream to salaries, distinct removes duplicate salary values, and skip(1) walks past the highest distinct salary. The sort is O(n log n), which is fine for interview-sized input and easy to read. For millions of salaries, I would use a one-pass loop that tracks the best two distinct values.
What interviewers follow up with: "Can you do it without sorting?" Say yes, then keep highest and secondHighest variables while rejecting duplicate salaries.
04Problem 2: frequency of each word, sorted by count desc then alphabetically
Interviewer prompt: "Given a sentence, count each word case-insensitively and print the result sorted by highest count, then alphabetically for ties."
Try it yourself first
Before reading the code, predict which order failed and payment should appear in when both have count 2.
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
final class Problem2WordFrequency {
public static void main(String[] args) {
String text = "Payment failed payment retried invoice failed";
Map<String, Long> counts = Pattern.compile("\\W+").splitAsStream(text.toLowerCase())
.filter(word -> !word.isBlank())
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry.comparingByKey()))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(left, right) -> left,
LinkedHashMap::new
));
System.out.println(counts); // {failed=2, payment=2, invoice=1, retried=1}
}
}
Why this works: groupingBy creates the frequency map, then the entry stream sorts by two rules. LinkedHashMap::new matters because the final map needs to remember the sorted iteration order. A plain HashMap would throw away the presentation order you just paid to compute.
What interviewers follow up with: "What about punctuation and Unicode?" I usually say this regex is enough for the exercise, but production tokenization needs a clearer language rule.
05Problem 3: first non-repeating character
Interviewer prompt: "Given a string, return the first character that appears exactly once. Preserve the original order."
Try it yourself first
Before reading on, answer for swiss. Then explain why a regular HashMap is risky for this problem.
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
final class Problem3FirstNonRepeatingCharacter {
public static void main(String[] args) {
String value = "swiss";
Map<Integer, Long> counts = value.codePoints().boxed()
.collect(Collectors.groupingBy(
Function.identity(),
LinkedHashMap::new,
Collectors.counting()
));
String first = counts.entrySet().stream()
.filter(entry -> entry.getValue() == 1)
.map(entry -> new String(Character.toChars(entry.getKey())))
.findFirst()
.orElseThrow();
System.out.println(first); // w
}
}
Why this works: LinkedHashMap keeps first-seen order, so the second stream sees characters in the same order as the input. I used codePoints() instead of chars() because it handles supplementary Unicode code points better than raw UTF-16 units. For ASCII interview input, chars() is usually accepted.
What interviewers follow up with: "Can you do it in one pass?" With a LinkedHashMap<Integer, Integer> and manual counts, yes. Pure streams make the order-preserving count readable, but not one-pass.
06Core collector problems: groups, duplicates and top totals
Now the prompts become more domain-shaped. This is where downstream collectors start doing the real work, which we cover in more depth in groupingBy and partitioningBy in depth.
07Problem 4: group employees by department with average salary
Interviewer prompt: "Given employees with department and salary, return the average salary per department. Make the output deterministic."
Try it yourself first
Before reading the code, name the classifier and the downstream collector separately.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
final class Problem4DepartmentAverageSalary {
record Employee(String name, String department, int salary) { }
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Asha", "Platform", 120_000),
new Employee("Ben", "Platform", 150_000),
new Employee("Chen", "Sales", 90_000),
new Employee("Diya", "Sales", 110_000)
);
Map<String, Double> averageByDepartment = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
TreeMap::new,
Collectors.averagingInt(Employee::salary)
));
System.out.println(averageByDepartment); // {Platform=135000.0, Sales=100000.0}
}
}
Why this works: Employee::department chooses the bucket, averagingInt decides the value inside each bucket, and TreeMap::new sorts department names. The result type is Double, so talk about rounding if money output is part of the requirement.
What interviewers follow up with: "Can you also return count and average?" Use summarizingInt or teeing, depending on the shape they want.
08Problem 5: find duplicates in a list
Interviewer prompt: "Given a list of order IDs, return each ID that appears more than once, in the order it first becomes a duplicate."
Try it yourself first
Before reading on, decide what should happen to the third copy of the same ID. Should it appear again?
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
final class Problem5FindDuplicates {
public static void main(String[] args) {
List<String> ids = List.of("o-7", "o-1", "o-7", "o-2", "o-1", "o-9");
Set<String> seen = new HashSet<>();
Set<String> duplicates = ids.stream()
.filter(id -> !seen.add(id))
.collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println(duplicates); // [o-7, o-1]
}
}
Why this works: Set.add returns false when the ID was already present, so the filter keeps only repeated IDs. The final LinkedHashSet prevents a third copy from appearing again. This is a stateful predicate, so keep it sequential and local to one pipeline.
What interviewers follow up with: "Is this safe in parallel?" No. Use a frequency map with groupingBy(..., counting()) if you need a more collector-shaped answer.
09Problem 6: top 3 customers by total order value
Interviewer prompt: "Given orders with customer IDs and amounts, return the top 3 customers by total spend. Break ties by customer ID."
Try it yourself first
Before reading the solution, say why sorting individual orders first is not enough.
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
final class Problem6TopCustomersByOrderValue {
record Order(String customerId, int cents) { }
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("c-1", 4000),
new Order("c-2", 7000),
new Order("c-1", 2500),
new Order("c-3", 9000),
new Order("c-4", 1000),
new Order("c-2", 1500)
);
List<Map.Entry<String, Integer>> topCustomers = orders.stream()
.collect(Collectors.groupingBy(
Order::customerId,
Collectors.summingInt(Order::cents)
))
.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry.comparingByKey()))
.limit(3)
.toList();
System.out.println(topCustomers); // [c-3=9000, c-2=8500, c-1=6500]
}
}
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Reshaping problems: flatten, partition and choose a best value
- Problem 7: flatten orders into distinct product names sorted alphabetically
- Problem 8: partition numbers into primes and non-primes
- Problem 9: longest string per group
- Harder problems: when streams start to creak
- Problem 10: merge two sorted lists via streams, and why a loop is better
- Problem 11: department with the highest headcount
- Problem 12: running total, and why streams are awkward
- Advanced patterns and interview trade-offs
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal