How do I use Collectors.groupingBy() with downstream collectors?

The Collectors.groupingBy is a powerful method in Java’s Stream API that allows grouping of elements in a stream based on a classification function, and it works well with downstream collectors. Here’s how you can use Collectors.groupingBy with downstream collectors effectively.


Syntax of Collectors.groupingBy with a Downstream Collector

The key method signature is:

Collectors.groupingBy(Classifier, Downstream)
  • Classifier: A function that determines how the elements are grouped (e.g., based on a key derived from the element).
  • Downstream Collector: The collector used to process the grouped elements further (e.g., counting, mapping, reducing, collecting to a list, etc.).

Example 1: Grouping Elements and Counting Them

To group elements based on a key and count the number of elements in each group:

Map<String, Long> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(), // Classifier
        Collectors.counting()       // Downstream collector
    ));
  • This produces a map where the key is the category, and the value is the count of items in that category.

Example 2: Group and Collect as a List

If you want to group the elements and collect them in lists:

Map<String, List<Item>> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(), // Classifier
        Collectors.toList()         // Downstream collector
    ));
  • Groups all elements into lists under their respective categories.

Example 3: Group and Use Summarizing Collector

To produce a statistical summary (e.g., count, sum, min, max, average) for each group:

Map<String, DoubleSummaryStatistics> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(), // Classifier
        Collectors.summarizingDouble(Item::getPrice) // Summarizing collector
    ));
  • This gives a map where each group has a DoubleSummaryStatistics object that includes the sum, count, min, max, and average for the prices in that group.

Example 4: Group and Reduce Values

To group elements and simultaneously reduce the values for each group:

Map<String, Optional<Item>> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(),                      // Classifier
        Collectors.reducing((item1, item2) -> 
            item1.getPrice() > item2.getPrice() ? item1 : item2) // Downstream: Find max price
    ));
  • This produces a map where each category has an Optional<Item> representing the item with the highest price.

Example 5: Multi-Level Grouping

You can nest multiple groupingBy collectors to perform hierarchical grouping:

Map<String, Map<String, List<Item>>> result = items.stream()
    .collect(Collectors.groupingBy(
        Item::getCategory,        // First-level group by category
        Collectors.groupingBy(Item::getType) // Second-level group by type
    ));
  • This creates a nested map where the first key is the category, and the value contains another map grouped by type.

Practical Example Walkthrough:

If you have a list of strings and want to:

  • Group them by their length.
  • Collect their counts using Collectors.counting().

Here’s how:

List<String> names = List.of("apple", "banana", "orange", "kiwi", "pear");

Map<Integer, Long> groupedCounts = names.stream()
    .collect(Collectors.groupingBy(
        String::length,       // Classifier: Group by string length
        Collectors.counting() // Downstream collector: Count elements
    ));

System.out.println(groupedCounts);
// Output: {4=2, 5=2, 6=1}

Key Points of Using Downstream Collectors:

  1. Flexibility: You can use different collectors (e.g., toList, toSet, counting, joining, etc.) to define how grouped elements are processed.
  2. Composition: Downstream collectors can be combined, nested, or customized using collectingAndThen or reducing.
  3. Extensibility: Custom Collector implementations can be used as downstream collectors for complex use cases.

This approach simplifies processing grouped data and eliminates the need for verbose loops or manual grouping logic.

How to use the Collectors.toUnmodifiableList() and other new Collectors in Java 10

In Java 10, a significant enhancement was introduced to the java.util.stream.Collectors class: new utility methods to create unmodifiable collections such as lists and sets. One notable method is Collectors.toUnmodifiableList(). This method allows you to efficiently create immutable lists during stream processing, adding to the immutability features provided by Java 9 and earlier versions.

Here’s how you can use Collectors.toUnmodifiableList() and other similar methods introduced in Java 10:


1. Using Collectors.toUnmodifiableList()

The Collectors.toUnmodifiableList() collector creates an unmodifiable list from a stream of elements. This means the resulting list cannot be modified (no adding, removing, or updating elements). If you attempt to modify it, a runtime exception (UnsupportedOperationException) will be thrown.

Example:

package org.kodejava.util.stream;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableList {
    public static void main(String[] args) {
        // Example list using Collectors.toUnmodifiableList
        List<String> unmodifiableList = Stream.of("A", "B", "C")
                .collect(Collectors.toUnmodifiableList());

        System.out.println("Unmodifiable List: " + unmodifiableList);

        // Attempt to modify the list will throw UnsupportedOperationException
        unmodifiableList.add("D"); // This will throw a runtime exception!
    }
}

Output:

Unmodifiable List: [A, B, C]
Exception in thread "main" java.lang.UnsupportedOperationException

2. Other Collectors Introduced in Java 10

Java 10 introduced two other collectors similar to toUnmodifiableList():

  • Collectors.toUnmodifiableSet()
    • Creates an unmodifiable set from a stream of elements.
    • Duplicate elements will be removed since it’s a set.

Example:

package org.kodejava.util.stream;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableSet {
    public static void main(String[] args) {
        Set<String> unmodifiableSet = Stream.of("A", "B", "C", "A") // "A" will appear only once
                .collect(Collectors.toUnmodifiableSet());
        System.out.println("Unmodifiable Set: " + unmodifiableSet);

        unmodifiableSet.add("D"); // Throws UnsupportedOperationException
    }
}
  • Collectors.toUnmodifiableMap()
    • Creates an unmodifiable map using key-value pairs from a stream.
    • Requires a way to specify the key and value in the collector.
    • If duplicate keys are produced, it will throw an IllegalStateException.

Example:

package org.kodejava.util.stream;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableMap {
    public static void main(String[] args) {
        Map<Integer, String> unmodifiableMap = Stream.of("A", "B", "C")
                .collect(Collectors.toUnmodifiableMap(
                        String::length,  // Key mapper
                        v -> v           // Value mapper
                ));

        System.out.println("Unmodifiable Map: " + unmodifiableMap);

        // Attempting to modify will throw an UnsupportedOperationException
        unmodifiableMap.put(2, "D"); // Throws UnsupportedOperationException
    }
}

3. Behavior of Unmodifiable Collections

  • These collectors guarantee that:
    • The collection cannot be modified (no add, remove, put, etc.).
    • Any attempt to modify them results in an UnsupportedOperationException.
    • They are safe to use for read-only purposes.
  • If the stream itself contains null values, a NullPointerException will be thrown.

4. Best Uses of toUnmodifiable*() Collectors

  • Ensuring immutability for collections to prevent accidental modifications.
  • Useful in multi-threaded or concurrent applications where immutability eliminates thread-safety issues.
  • Perfect for cases where only read access is required.

5. Comparison with Java 9 List.of()

Java 9 introduced factory methods like List.of(), Set.of(), and Map.of() for creating immutable collections. While those methods are concise, the new collectors offer more flexibility when working with streams.

Java 9 Example:

List<String> immutableList = List.of("A", "B", "C");

Java 10 Stream Example:

List<String> immutableList = Stream.of("A", "B", "C")
                                   .collect(Collectors.toUnmodifiableList());

Summary Table:

Collector Description Introduced in
Collectors.toUnmodifiableList() Creates an unmodifiable List Java 10
Collectors.toUnmodifiableSet() Creates an unmodifiable Set Java 10
Collectors.toUnmodifiableMap() Creates an unmodifiable Map Java 10

Conclusion

The Collectors.toUnmodifiableList() and related methods introduced in Java 10 are powerful tools for creating immutable collections directly from streams. They ensure immutability, improve code safety, and fit well into functional programming paradigms introduced with Java Streams.

How do I add an object to the beginning of Stream?

To add an object to the beginning of a list using Java Streams, we typically cannot directly prepend an object in a stream-friendly way because Streams themselves are immutable and don’t directly modify the original collection. However, we can achieve this by creating a new list with the desired order.

Here’s how we can approach it:

package org.kodejava.stream;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamBeginningAdd {
    public static void main(String[] args) {
        List<String> originalList = Arrays.asList("B", "C", "D");
        String newElement = "A";

        // Add the new element at the beginning using Stream
        List<String> updatedList = Stream.concat(Stream.of(newElement), originalList.stream())
                .collect(Collectors.toList());

        // Output: [A, B, C, D]
        System.out.println(updatedList);
    }
}

Explanation:

  1. Stream.of(newElement): Wraps the new element as a single-element stream.
  2. originalList.stream(): Converts the existing list into a stream.
  3. Stream.concat(): Combines the two streams — placing the newElement stream first and the original list’s stream second.
  4. collect(Collectors.toList()): Materializes (collects) the combined stream into a new list.

This ensures immutability of the original list and creates a new list with the desired order.

How do I sum a BigDecimal property of a list of objects using Java Stream API?

If we have a list of objects, and we want to sum a BigDecimal property of these objects, we can achieve this using the Java Stream API. This API provides a clean and efficient way to process collections of objects. To sum the BigDecimal amounts, you can use the map and reduce methods of the Stream API.

As an example, we have a class named Transaction with a BigDecimal property named amount. We have a list of Transaction objects, and we want to calculate the total sum of the amount properties.

In the code snippet below we do the following:

  • Creating Transactions: We create a list of Transaction objects, each with a different BigDecimal amount.
  • Filter Transactions and its amount: We filter to exclude the null transaction and null transaction amount.
  • Mapping to Amounts: We use the map method to convert each Transaction object to its amount property.
  • Summing the Amounts: The reduce method takes two parameters: an identity value (BigDecimal.ZERO) and an accumulator function (BigDecimal::add). The accumulator function adds each BigDecimal in the stream to the running total.
  • Printing the Result: Finally, we print the total sum of the amounts.
package org.kodejava.stream;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;

public class BigDecimalSumExample {
    public static void main(String[] args) {
        // Create a list of transaction objects
        List<Transaction> transactions = Arrays.asList(
                new Transaction(new BigDecimal("10.50")),
                null,
                new Transaction(new BigDecimal("30.25")),
                new Transaction(null),
                new Transaction(new BigDecimal("11.49"))
        );

        // Sum the amount properties using stream
        BigDecimal totalAmount = transactions.stream()
                // Filter out null Transaction objects and Transaction objects
                // with null amounts
                .filter(t -> t != null && t.getAmount() != null)
                .map(Transaction::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // Print the result
        System.out.println("Total Amount: " + totalAmount);
    }

    static class Transaction {
        private final BigDecimal amount;

        public Transaction(BigDecimal amount) {
            this.amount = amount;
        }

        public BigDecimal getAmount() {
            return amount;
        }
    }
}

Below is another example, we want to sum just a List<BigDecimal> values. To sum the values we can use the reduce method as shown in the code snippet below.

package org.kodejava.stream;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public class BigDecimalListSumExample {
    public static void main(String[] args) {
        // Create a list of BigDecimal values
        List<BigDecimal> amounts = Arrays.asList(
                new BigDecimal("10.50"),
                new BigDecimal("20.75"),
                new BigDecimal("30.25"),
                null,
                new BigDecimal("11.49")
        );

        // Sum the BigDecimal values using stream
        BigDecimal totalAmount = amounts.stream()
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // Print the result
        System.out.println("Total Amount: " + totalAmount);
    }
}

Using Java Stream API to sum a BigDecimal property of a list of objects or a list of BigDecimal values are both concise and efficient. The map and reduce methods streamline the process, making our code more readable and maintainable. This approach can be applied to various scenarios where we need to aggregate data from a list of objects.

How do I use Collectors.maxBy() method?

The Collectors.maxBy() method is used to find the maximum element from a stream based on a certain comparator. It returns an Optional which contains the maximum element according to the provided comparator, or an empty Optional if there are no elements in the stream.

Here’s a simple example where we have a list of integers, and we want to find the biggest integer:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class MaxByDemo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        Optional<Integer> maxNumber = numbers.stream()
                .collect(Collectors.maxBy(Comparator.naturalOrder()));

        maxNumber.ifPresent(System.out::println);
    }
}

In this example:

  • We create a Stream from the list of integers.
  • We then use Collectors.maxBy(Comparator.naturalOrder()) to get the maximum number. Comparator.naturalOrder() is a shortcut for Comparator.comparing(Function.identity()).
  • Collectors.maxBy() returns an Optional because the stream could be empty.
  • We print the maximum number if it exists.

When you run this program, it will print “5” because 5 is the biggest number in the list.

Keep in mind that if the stream is empty, maxNumber will be an empty Optional, and nothing will be printed.