Ten hands-on Stream API problems — filter evens, max, sum, uppercase, sort, count, distinct, reduce, findAny and extracting first names — with correct solutions, edge cases and the follow-ups interviewers ask.
Published September 25, 2026
In a live coding round you'll often be asked to "solve it with streams". Write the pipeline, then say one sentence about each operation, and mention an edge case (empty list, nulls, ties). All the examples use this data:
List<Integer> numbers = List.of(4, 9, 1, 12, 7, 4, 6);
List<String> names = List.of("Asha Rao", "Vikram Singh", "Meera Iyer");
stream.toList() (Java 16+) returns an unmodifiable list. Use collect(Collectors.toList()) on Java 8–15, or when the caller needs to modify the result.
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.toList(); // [4, 12, 4, 6]
Explain: filter keeps the elements that match the predicate, and toList collects them.
Key points to cover:
n % 2 == 0 works for negative numbers too. The odd check is the one to be careful with: n % 2 == 1 fails for negative odd numbers (−3 % 2 is −1). Use n % 2 != 0.int[]: Arrays.stream(arr).filter(n -> n % 2 == 0).toArray(), which avoids boxing.Learn it in depth → Streams API
Optional<Integer> max = numbers.stream().max(Comparator.naturalOrder()); // Optional[12]
int maxOrZero = numbers.stream().mapToInt(Integer::intValue).max().orElse(0);
Explain: max takes a comparator and returns an Optional, because the list might be empty.
Key points to cover:
Integer::compare also works as the comparator.(a, b) -> a - b, because subtraction can overflow.employees.stream().max(Comparator.comparing(Employee::salary)).int sum = numbers.stream().mapToInt(Integer::intValue).sum(); // 43
long safeSum = numbers.stream().mapToLong(Integer::longValue).sum();
Explain: mapToInt converts to an IntStream, which has sum(), average() and friends, with no boxing.
Key points to cover:
mapToLong when the total could exceed Integer.MAX_VALUE. IntStream.sum() overflows silently.BigDecimals with reduce(BigDecimal.ZERO, BigDecimal::add).List<String> upper = names.stream()
.map(n -> n.toUpperCase(Locale.ROOT))
.toList();
Explain: map transforms each element, and the result has the same size.
Key points to cover:
Locale for predictable results. "title".toUpperCase() behaves unexpectedly in a Turkish locale (dotted/dotless i). String::toUpperCase is fine for a quick answer.List<Integer> asc = numbers.stream().sorted().toList(); // [1, 4, 4, 6, 7, 9, 12]
List<Integer> desc = numbers.stream().sorted(Comparator.reverseOrder()).toList(); // [12, 9, 7, 6, 4, 4, 1]
List<Employee> bySalaryThenName = employees.stream()
.sorted(Comparator.comparing(Employee::salary).reversed().thenComparing(Employee::name))
.toList();
Explain: sorted() uses the natural order. Pass a comparator for a custom order.
Key points to cover:
sorted is a stateful intermediate operation: it has to buffer every element before it emits any.list.sort(...) on a mutable list.Learn it in depth → Collectors
long count = numbers.stream().filter(n -> n > 5).count(); // 4 (9, 12, 7, 6)
Explain: count is a terminal operation that returns a long.
Key points to cover:
count() may skip running the pipeline entirely if it can compute the size directly, for example on a sized stream with no filter. So side effects in peek or map might not run. Don't rely on them.List<Integer> unique = numbers.stream().distinct().toList(); // [4, 9, 1, 12, 7, 6]: first-seen order kept
Explain: distinct removes duplicates using equals/hashCode, and it keeps encounter order for ordered streams.
Key points to cover:
distinct relies on equals/hashCode. To deduplicate by a property, collect into a map: toMap(Employee::email, e -> e, (a, b) -> a), then take .values().reduce.int total = numbers.stream().reduce(0, Integer::sum); // 43
Optional<Integer> product = numbers.stream().reduce((a, b) -> a * b);
Explain: reduce(identity, accumulator) folds the elements into one value, starting from the identity.
Key points to cover:
Optional, because the stream could be empty.mapToInt(...).sum() is clearer and avoids boxing.Optional<Integer> any = numbers.stream().findAny();
Explain: findAny returns some element. It short-circuits, and it's empty for an empty stream.
Key points to cover:
findFirst.findFirst when the order matters.List<String> firstNames = names.stream()
.map(String::strip)
.filter(n -> !n.isEmpty())
.map(n -> n.split("\\s+")[0])
.toList(); // [Asha, Vikram, Meera]
Explain: split each name on whitespace, and take the first part.
Key points to cover:
split(" ") breaks on double spaces ("Vikram Singh"), and "".split(...) still returns one element. The regex \\s+ together with strip handles messy input.n.substring(0, n.indexOf(' ')), but that fails when there's no space, so guard it.Q: What's the difference between Collectors.toList() and Stream.toList()?
A: Stream.toList() (Java 16) returns an unmodifiable list, and allows null elements. Collectors.toList() returns a mutable ArrayList in practice, although that isn't guaranteed by its contract. Use Collectors.toCollection(ArrayList::new) when you need a specific mutable type.
Q: How do you find the second-highest number with streams?
A: numbers.stream().distinct().sorted(Comparator.reverseOrder()).skip(1).findFirst(). It returns an Optional, which is empty if there are fewer than two distinct values.
Q: How do you get the average?
A: numbers.stream().mapToInt(Integer::intValue).average(), which returns an OptionalDouble. Or Collectors.averagingInt(...) inside a collect.
Q: Can a stream be reused?
A: No. After a terminal operation it's consumed, and reusing it throws an IllegalStateException. Create a new stream from the source, or use a Supplier<Stream<T>>.