1. Introduction

In this tutorial, we’ll see how to group elements by value and find their minimum and maximum values in each group.

We’ll need a basic knowledge of Java 8 streams to understand this tutorial better. Also, for more details, check grouping collectors.

2. Understanding the Use Case

Suppose we have a list of order items with category and price fields, and we would like to group the items by category and find the cheaper and costlier items in each category.

Let’s see the order item categories:

public enum OrderItemCategory {
    BOOKS, CLOTHING, ELECTRONICS, FURNITURE, OTHER;
}

Now, let’s define the order item:

public class OrderItem {
    private Long id;
    private Double price;
    private OrderItemCategory category;
    // other fields
    // getters and setters
}

3. Grouping the Order Items Using Streams API

Now, let’s see how to group the ordered items by category and find the items with minimum and maximum prices in each group.

Implementation of grouping logic:

Map<OrderItemCategory, Pair<Double, Double>> groupByCategoryWithMinMax(List<OrderItem> orderItems) {
    Map<OrderItemCategory, DoubleSummaryStatistics> categoryStatistics = orderItems.stream()
      .collect(Collectors.groupingBy(OrderItem::getCategory, Collectors.summarizingDouble(OrderItem::getPrice)));

    return categoryStatistics.entrySet().stream()
      .collect(Collectors.toMap(Map.Entry::getKey, entry -> Pair.of(entry.getValue().getMin(), entry.getValue().getMax())));
}

The OrderProcessor class contains a method groupByCategoryWithMinMax() that processes a list of OrderItem objects. This method groups the items by category and calculates each category’s minimum and maximum prices.

First, it uses Java streams to gather category statistics. It groups items by category using Collectors.groupingBy, and summarizes prices with Collectors.summarizingDouble.

It creates a map where the key is the OrderItemCategory and the value is a DoubleSummaryStatistics object, containing the count, sum, average, min, and max of the prices.

Next, the method transforms this map into another map where the key remains the OrderItemCategory, but the value is a Pair<Double, Double>. This pair holds the minimum and maximum prices, extracted from the DoubleSummaryStatistics for each category.

3.1. Understanding DoubleSummaryStatistics

DoubleSummaryStatistics provides a way to collect and summarize statistical data from a stream of double values.  When using streams, especially for tasks like grouping or summarizing, it’s beneficial to capture aggregate data such as count, sum, average, min, and max in one pass.

Use Collectors.summarizingDouble with stream’s data to efficiently gather statistics such as minimum and maximum prices per category without manual computation. DoubleSummaryStatistics aggregates multiple statistics in one pass, optimizing performance, especially for large datasets.

In the example, we used the groupByCategoryWithMinMax() method to compute the minimum and maximum prices for each category. Then we stored the results on a map. Finally, we converted the map to another map, associating each category with a Pair of minimum and maximum prices.

3.2. Understanding summarizingDouble Collector

The summarizingDouble collector gathers statistical information for a set of double values. It collects data such as count, sum, average, minimum, and maximum in a single pass. This collector is part of the Collectors utility class and returns a DoubleSummaryStatistics object.

It is efficient to use summarizingDouble because it avoids multiple passes over the data. That makes it ideal for large datasets. It simplifies the process of obtaining comprehensive statistics by encapsulating all the necessary calculations within the DoubleSummaryStatistics object.

4. Testing Grouping by Logic

Let’s check our grouping logic with a test:

@Test
void whenOrderItemsAreGrouped_thenGetsMinMaxPerGroup() {
    List<OrderItem> items =
      Arrays.asList(
        new OrderItem(1L, OrderItemCategory.ELECTRONICS, 1299.99),
        new OrderItem(2L, OrderItemCategory.ELECTRONICS, 1199.99),
        new OrderItem(3L, OrderItemCategory.ELECTRONICS, 2199.99),
        new OrderItem(4L, OrderItemCategory.FURNITURE, 220.00),
        new OrderItem(4L, OrderItemCategory.FURNITURE, 200.20),
        new OrderItem(5L, OrderItemCategory.FURNITURE, 215.00),
        new OrderItem(6L, OrderItemCategory.CLOTHING, 50.75),
        new OrderItem(7L, OrderItemCategory.CLOTHING, 75.00),
        new OrderItem(8L, OrderItemCategory.CLOTHING, 75.00));

    OrderProcessor orderProcessor = new OrderProcessor();
    Map<OrderItemCategory, Pair<Double, Double>> orderItemCategoryPairMap =
      orderProcessor.groupByCategoryWithMinMax(items);
    assertEquals(orderItemCategoryPairMap.get(OrderItemCategory.ELECTRONICS), Pair.of(1199.99, 2199.99));
    assertEquals(orderItemCategoryPairMap.get(OrderItemCategory.FURNITURE), Pair.of(200.20, 220.00));
    assertEquals(orderItemCategoryPairMap.get(OrderItemCategory.CLOTHING), Pair.of(50.75, 75.00));
}

The OrderProcessorUnitTest class tests the groupByCategoryWithMinMax() method. It creates a list of OrderItem objects, each with a category and price. Then it calls the groupByCategoryWithMinMax() method to group these items by category and determine the minimum and maximum prices for each group. The test then verifies that the results match the expected minimum and maximum prices for each category using assertions.

5. Conclusion

In this article, we learned to group stream items and find the minimum and maximum in each group.

As always, the source code for the examples is available over on GitHub.