Java Streams and Collectors Deep Quiz
A 4-question reference set on Java streams beyond the basics: laziness and short-circuiting, downstream collectors in groupingBy, toMap collision handling, and when parallel streams actually pay off.
By CodeSnatch
December 6, 2025
·
Updated August 11, 2026
237 views
4
Rate
Java streams are lazy: intermediate operations defer until a terminal operation fires. What does this enable, and where does it bite when paired with side effects?
Examples
Example 1:
Input: list.stream().filter(x -> x > 100).map(this::loadOrder).findFirst()
Output: Stream short-circuits after the first match; loadOrder is called only once
Explanation: Laziness means filter+map fuse into one pass and stop at the terminal's first satisfied element.Example 2:
Input: list.stream().peek(x -> log.info("seen {}", x)).filter(x -> x > 100).collect(toList())
Output: peek runs for every element, but ONLY because there's a downstream filter+collect; without a terminal op, peek runs zero times
Explanation: Side-effect-inside-stream is a smell; if the terminal op short-circuits or is removed, peek behavior changes.import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class Main {
public record Order(int id) {}
public Optional<Order> firstLargeOrder(List<Integer> ids) {
return ids.stream()
.filter(id -> id > 100)
.map(this::loadOrder)
.findFirst();
}
public List<Order> allLargeOrders(List<Integer> ids) {
return ids.stream()
.filter(id -> id > 100)
.map(this::loadOrder)
.collect(Collectors.toList());
}
private Order loadOrder(int id) {
return new Order(id);
}
public static void main(String[] args) {
Main m = new Main();
List<Integer> ids = List.of(1, 50, 200, 300, 75, 400);
System.out.println("first: " + m.firstLargeOrder(ids));
System.out.println("all: " + m.allLargeOrders(ids));
}
}Collectors.groupingBy returns Map<K, List<V>> by default but accepts a downstream collector. Walk through three common downstream collectors and the shape they produce.
Examples
Example 1:
Input: orders.stream().collect(Collectors.groupingBy(Order::status))
Output: Map<Status, List<Order>> // default downstream is toList()
Explanation: One-argument groupingBy buckets elements; the downstream value is a list.Example 2:
Input: orders.stream().collect(Collectors.groupingBy(Order::status, Collectors.counting()))
Output: Map<Status, Long>
Explanation: Downstream Collectors.counting() reduces each group to its size.import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
public record Order(int id, String status, double amount) {}
public Map<String, List<Order>> ordersByStatus(List<Order> orders) {
return orders.stream()
.collect(Collectors.groupingBy(Order::status));
}
public Map<String, Long> countByStatus(List<Order> orders) {
return orders.stream()
.collect(Collectors.groupingBy(Order::status, Collectors.counting()));
}
public Map<String, Double> avgAmountByStatus(List<Order> orders) {
return orders.stream()
.collect(Collectors.groupingBy(
Order::status,
Collectors.averagingDouble(Order::amount)
));
}
public Map<String, List<Integer>> idsByStatus(List<Order> orders) {
return orders.stream()
.collect(Collectors.groupingBy(
Order::status,
Collectors.mapping(Order::id, Collectors.toList())
));
}
public static void main(String[] args) {
Main m = new Main();
List<Order> orders = List.of(
new Order(1, "PAID", 10.0),
new Order(2, "PAID", 20.0),
new Order(3, "PENDING", 5.0)
);
System.out.println("countByStatus: " + m.countByStatus(orders));
System.out.println("avgAmountByStatus: " + m.avgAmountByStatus(orders));
}
}Collectors.toMap throws on duplicate keys by default. What is the merge function for, and what is the standard fix when keys can collide?
Examples
Example 1:
Input: orders.stream().collect(Collectors.toMap(Order::userId, Order::amount))
Output: Throws IllegalStateException if two orders have the same userId
Explanation: Two-arg toMap assumes keys are unique; collision throws to surface the bug.Example 2:
Input: orders.stream().collect(Collectors.toMap(Order::userId, Order::amount, Double::sum))
Output: Map<UserId, Double> with summed amounts per user
Explanation: The third argument is a BinaryOperator that resolves collisions.import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
public record Order(int userId, double amount, long createdAt) {}
public Map<Integer, Double> totalsByUser(List<Order> orders) {
return orders.stream()
.collect(Collectors.toMap(
Order::userId,
Order::amount,
Double::sum
));
}
public Map<Integer, Order> latestPerUser(List<Order> orders) {
return orders.stream()
.collect(Collectors.toMap(
Order::userId,
o -> o,
(existing, replacement) -> existing.createdAt() > replacement.createdAt() ? existing : replacement
));
}
public Map<Integer, Order> firstPerUserAsLinked(List<Order> orders) {
return orders.stream()
.collect(Collectors.toMap(
Order::userId,
o -> o,
(existing, replacement) -> existing,
java.util.LinkedHashMap::new
));
}
public static void main(String[] args) {
Main m = new Main();
List<Order> orders = List.of(
new Order(1, 10.0, 100),
new Order(1, 20.0, 200),
new Order(2, 5.0, 150)
);
System.out.println("totals: " + m.totalsByUser(orders));
System.out.println("latest: " + m.latestPerUser(orders));
}
}Parallel streams use the common ForkJoinPool. When does .parallel() actually help, and what is the right way to scale it for I/O?
Examples
Example 1:
Input: 10M integers in a list; .parallelStream().mapToInt(...).sum()
Output: ~Nx speedup where N is core count; CPU-bound numeric work parallelizes well
Explanation: Stream parallelism shines on CPU-bound, side-effect-free reductions over large in-memory collections.Example 2:
Input: 100 URLs; .parallelStream().map(url -> httpGet(url)).collect(toList())
Output: Limited speedup; ForkJoinPool.commonPool() has only Runtime.getRuntime().availableProcessors()-1 threads
Explanation: Common pool is sized for CPU; I/O parallelism needs a custom pool or a different abstraction.import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
public class Main {
public long sumSquares(List<Integer> values) {
return values.parallelStream()
.mapToLong(v -> (long) v * v)
.sum();
}
public List<String> fetchAllOnCustomPool(List<String> urls) throws Exception {
ForkJoinPool pool = new ForkJoinPool(32);
try {
return pool.submit(() ->
urls.parallelStream()
.map(this::httpGet)
.collect(Collectors.toList())
).get();
} finally {
pool.shutdown();
}
}
public List<String> fetchAllWithFutures(List<String> urls, java.util.concurrent.Executor exec) {
List<CompletableFuture<String>> futures = urls.stream()
.map(u -> CompletableFuture.supplyAsync(() -> httpGet(u), exec))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
private String httpGet(String url) {
return url;
}
public static void main(String[] args) {
Main m = new Main();
System.out.println("sumSquares: " + m.sumSquares(List.of(1, 2, 3, 4, 5)));
}
}