Skip to Content
Course content

267: Practice Exercise: Grouping and Aggregating Sales Data with Streams

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 region field.
  • Sum the amount for 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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.