Most stream tutorials stop at filter and map. Real codebases lean on a small set of more interesting patterns. Here are the ten we see most in production Java, with the pitfalls that come with them.
All examples use this record:
record Order(long id, String customer, String status, long amount, LocalDate date) {}1. Build a lookup map by ID
Turning a list into a map for O(1) lookups is the single most common collector:
Map<Long, Order> byId = orders.stream()
.collect(Collectors.toMap(Order::id, Function.identity()));Common mistake
toMap throws IllegalStateException on duplicate keys. If duplicates are possible, pass a merge function: toMap(Order::customer, o -> o, (a, b) -> a).
2. Group into lists
Map<String, List<Order>> byCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::customer));3. Group and aggregate
Pass a downstream collector to aggregate each group instead of collecting lists:
Map<String, Long> revenueByCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::customer, Collectors.summingLong(Order::amount)));
Map<String, Long> countByStatus = orders.stream()
.collect(Collectors.groupingBy(Order::status, Collectors.counting()));4. Split into two with partitioningBy
When the grouping is a yes/no question, partitioningBy always returns both keys, even when one side is empty:
Map<Boolean, List<Order>> bigAndSmall = orders.stream()
.collect(Collectors.partitioningBy(o -> o.amount() > 10_000));5. Top-N
List<Order> top5 = orders.stream()
.sorted(Comparator.comparingLong(Order::amount).reversed())
.limit(5)
.toList();6. Find the max per group
Map<String, Optional<Order>> biggestPerCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::customer,
Collectors.maxBy(Comparator.comparingLong(Order::amount))));7. Deduplicate by a field
distinct() uses equals, so it can't dedupe by a single field. Collect into a map keyed by that field instead:
Collection<Order> onePerCustomer = orders.stream()
.collect(Collectors.toMap(Order::customer, o -> o, (first, second) -> first, LinkedHashMap::new))
.values();8. Flatten nested collections
List<String> allTags = articles.stream()
.flatMap(a -> a.tags().stream())
.distinct()
.toList();9. Join strings
String csv = orders.stream()
.map(o -> String.valueOf(o.id()))
.collect(Collectors.joining(", ", "[", "]"));
// [101, 102, 103]10. Paginate an in-memory list
int page = 2, size = 20;
List<Order> pageItems = orders.stream()
.skip((long) page * size)
.limit(size)
.toList();For database data, paginate in the query — never load everything and page in memory.
Common mistakes
Using forEach to build collections
// Don't
List<String> names = new ArrayList<>();
orders.stream().forEach(o -> names.add(o.customer()));
// Do
List<String> names = orders.stream().map(Order::customer).toList();Using streams for everything
A plain for loop is clearer when you need early return, checked exceptions, or to mutate several variables at once. Streams are a tool, not a rule.
Reaching for parallelStream()
Parallel streams share the common ForkJoinPool with the rest of your app and only pay off for large, CPU-bound work. In a web server they usually make latency worse.
Summary
| Need | Reach for |
|---|---|
| Lookup by key | toMap |
| Group | groupingBy |
| Group + total | groupingBy + summingLong / counting |
| Yes/no split | partitioningBy |
| Top-N | sorted + limit |
| Flatten | flatMap |
| Build a string | joining |
Want to go deeper?
Continue your learning journey
This article covers Java 8+ on the Java Full Stack roadmap. Follow the path to know exactly what comes next.
- Collections
- Java 8+You’re reading
- Spring Boot
- REST APIs