Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
73: Collectors in Depth
I remember a junior developer on my team, let's call him Sam, who spent nearly half a day writing a reporting module for an e-commerce backend. He was trying to group orders by their shipping region and then, within each region, find the most expensive item sold. I looked at his PR and saw about 50 lines of nested for loops, manual Map instantiations, and a lot of "if key not present, create new list" boilerplate. It wasn't wrong—it worked—but it was fragile and exhausted to read. I sat down with him and showed him how to use Collectors.groupingBy combined with a downstream collector. We collapsed those 50 lines into a single, readable stream pipeline. Sam looked at me like I'd just performed a magic trick, but it wasn't magic; it was just leveraging the full power of the Collectors class.
Categorizing Data with groupingBy and partitioningBy
Most of us start our Stream journey with .collect(Collectors.toList()), but that's barely scratching the surface. The real heavy lifting happens when you need to reorganize your data. Collectors.groupingBy is your primary tool here. It functions similarly to a SQL GROUP BY clause, taking a classifier function and returning a Map where the keys are the results of that function.
List<Employee> employees = getEmployeeList();
// Simple grouping: Map<Department, List<Employee>>
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Now, there's a specialized version of this called partitioningBy. I always tell developers to use this instead of groupingBy when the logic is a simple true/false split. It's more efficient and returns a Map<Boolean, List<T>>. If you're splitting a list into "active" and "inactive" users, partitioningBy is the semantically correct choice.
// Splitting employees into those making over 100k and those who aren't
Map<Boolean, List<Employee>> highEarners = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 100_000));
Layering Collectors for Complex Reductions
This is where things get interesting. The groupingBy method is overloaded, meaning you can pass a second collector as a "downstream" operation. Instead of just getting a List of items for every key, you can transform that list into something else—a count, a sum, or even another map—before the final map is ever returned.
Imagine you don't want a list of all employees in a department; you just want the average salary for that department. You can nest Collectors.averagingDouble inside your groupingBy. This prevents the overhead of creating intermediate lists that you're just going to throw away anyway.
Map<Department, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
If you need something even more specific, like the name of the highest-paid person per department, you can use Collectors.maxBy. Be careful here, though: maxBy returns an Optional, so your resulting map will be Map<Department, Optional<Employee>>. If that's too clunky, you can wrap the whole thing in Collectors.collectingAndThen to extract the value from the optional immediately.
I've found that once you get comfortable with downstream collectors, you stop thinking in terms of "how do I loop through this" and start thinking in terms of "how do I shape this data." It's a fundamental shift in how you approach data transformation in Java.
📋 Practical Task
Building a Regional Sales Performance Report
You are tasked with analyzing a list of Sale objects. Each Sale has a region (String), a category (String), and an amount (double).
Write a program that uses a single Stream pipeline to produce a Map where:
- The key is the region.
- The value is another
Mapwhere the key is the category and the value is the total sum of sales for that category in that region.
Requirements:
- Use
Collectors.groupingBynested within anotherCollectors.groupingBy. - Use
Collectors.summingDoubleas the final downstream collector. - Your final result type should be
Map<String, Map<String, Double>>.
Starter Code:
public class Sale {
private final String region;
private final String category;
private final double amount;
public Sale(String region, String category, double amount) {
this.region = region;
this.category = category;
this.amount = amount;
}
public String getRegion() { return region; }
public String getCategory() { return category; }
public double getAmount() { return amount; }
}There are no comments for now.