-
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
267: Practice Exercise: Grouping and Aggregating Sales Data with Streams
I've seen this exact pattern a dozen times when developers first start using Collectors.groupingBy. They have a list of data and they want a summary—like total revenue per category—but they end up with a map of lists and a lot of frustration.
Take a look at this snippet. The goal here is to take a list of Sale objects and find the total dollar amount sold for each category.
Map<String, Double> totals = sales.stream()
.collect(Collectors.groupingBy(Sale::getCategory));
// Error: Incompatible types. Found Map<String, List<Sale>> required Map<String, Double>
The "List instead of Total" Trap
If you try to compile that, the compiler will rightfully scream at you. The problem is that groupingBy(classifier) is a shorthand. When you only provide the classification function (in this case, the category), Java assumes you want a Map where the key is the category and the value is a List of every single Sale object that matched that category.
It's a useful default, but it's not an aggregation. You haven't actually told Java to sum anything; you've just told it to sort the items into buckets. You're left with a Map<String, List<Sale>>, but you wanted a Map<String, Double>.
Using Downstream Collectors for Aggregation
To fix this, we need to use the overloaded version of groupingBy that takes a second argument: a "downstream collector." Think of the first argument as the "bucket creator" and the second argument as "what to do with the items once they are in the bucket."
Instead of letting Java default to a toList() collector, we'll pass in Collectors.summingDouble(). This tells the stream: "Group these by category, then for every group, extract the price and sum it up."
Map<String, Double> totals = sales.stream()
.collect(Collectors.groupingBy(
Sale::getCategory,
Collectors.summingDouble(Sale::getAmount)
));
Now the types align. The groupingBy handles the keys, and summingDouble handles the values. I usually prefer summingDouble or summingInt over trying to use a reducing collector here because they are much more readable and handle the initial identity value (zero) for you automatically.
Handling Complex Aggregations with Mapping
Sometimes, you don't just want a sum; you might want to count unique customers per category or find the average price. If the data you need to aggregate isn't a simple numeric field on the object, you can chain a mapping() collector before the aggregation.
For instance, if you wanted to group by category but count only the unique customerIds associated with those sales, you'd do something like this:
Map<String, Long> uniqueCustomersPerCategory = sales.stream()
.collect(Collectors.groupingBy(
Sale::getCategory,
Collectors.mapping(Sale::getCustomerId, Collectors.counting())
// Note: This counts total sales; for unique, you'd collect to a Set first.
));
Wait—I just caught myself. If you actually want unique counts, counting() alone won't work because it counts every element in the stream. To get unique counts, you'd typically collect to a Set first, or use a different approach. This is where streams can get verbose, which is why I always recommend sketching out your "downstream" logic on paper before typing it into the IDE.
📋 Practical Task
Exercise: Generating a Regional Sales Performance Report
You are tasked with creating a report that summarizes sales data. You have a Sale class with the following properties: String region, String product, and double amount.
Write a method public Map<String, Double> calculateRegionalTotals(List<Sale> sales) that uses Java Streams to:
- Group the sales by the
regionfield. - Sum the
amountfor all sales within each region. - Return the resulting map where the key is the region name and the value is the total revenue for that region.
Constraints: You must complete this using a single .collect() call with a downstream collector. Do not use manual forEach loops to populate the map.
There are no comments for now.