Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Collecting Streams: Gathering Your Treasure Trove

Header Image

Ahoy there, mateys! So far, we’ve been exploring the vast and treacherous waters of Java Streams. We’ve learned about the purpose of Java Streams, their advantages over traditional Java collections, and the magic of stream pipelines. But what do we do once we’ve successfully navigated our way through those waters? We collect our bounty, of course! In this article, we’ll be delving into the art of collecting stream elements into collections. Let’s hoist the Jolly Roger and set sail!

Creating Collections of Stream Elements

In previous articles, we’ve learned how to create streams from collections and arrays. However, streams are not meant to be traversed alone. We need to gather our stream elements into a collection to make them more accessible and easier to work with. Luckily, Java Streams provide us with the collect method to do just that.

The collect method is a terminal operation that gathers the elements of a stream into a collection. It can create various types of collections such as lists, sets, and maps, among others. The collect method is called on the stream, and it takes a Collector as an argument. A Collector is an interface that specifies how the elements should be collected into the desired type of collection.

Collecting Stream Elements into Lists

Let’s start by collecting stream elements into a list. We’ll use the toList method of the Collectors class to create a list of elements. Here’s an example:

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());

System.out.println(evenNumbers); // Output: [2, 4]

In this example, we first create a list of integers, numbers. Then, we create a new list, evenNumbers, by filtering the elements of the numbers list to include only even numbers. Finally, we collect the filtered elements into a list using the toList method of the Collectors class. The result is a list of even numbers.

Collecting Stream Elements into Sets

We can also collect stream elements into a set. A set is a collection that contains no duplicates, making it useful when we want to remove duplicates from a stream. We’ll use the toSet method of the Collectors class to create a set of elements. Here’s an example:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 4, 3, 2, 1);
Set<Integer> uniqueNumbers = numbers.stream()
                                     .collect(Collectors.toSet());

System.out.println(uniqueNumbers); // Output: [1, 2, 3, 4, 5]

In this example, we first create a list of integers, numbers, that contains duplicates. Then, we create a new set, uniqueNumbers, by collecting the elements of the numbers list into a set using the toSet method of the Collectors class. The result is a set of unique integers.

Collecting Stream Elements into Maps

We can also collect stream elements into a map. A map is a collection that maps keys to values, making it useful when we want to associate values with specific keys. We’ll use the toMap method of the Collectors class to create a map of elements. Here’s an example:

List<String> words = List.of("apple", "banana", "cherry", "date"); Map<Character, String> wordMap = words.stream() .collect(Collectors.toMap(s -> s.charAt(0), s -> s)); System.out.println(wordMap); // Output: {a=apple, b=banana, c=cherry, d=date} ``` In this example, we first create a list of strings, `words`. Then, we create a new map, `wordMap`, by collecting the elements of the `words` list into a map using the `toMap` method of the `Collectors` class. We map the first character of each word to the word itself. The result is a map that associates the first character of each word with the word itself. ## Custom Collectors for Stream Elements Sometimes, the collectors provided by the `Collectors` class may not suit our needs. In such cases, we can create custom collectors that specify exactly how the elements should be collected. To create a custom collector, we need to implement the `Collector` interface. The `Collector` interface has four methods that we need to implement: `supplier`, `accumulator`, `combiner`, and `finisher`. The `supplier` method creates a new mutable container that will hold the elements being collected. The `accumulator` method adds an element to the container. The `combiner` method combines two containers into one. Finally, the `finisher` method transforms the container into the final result. Let's see an example of a custom collector. Suppose we have a stream of strings, and we want to concatenate all the strings into a single string, separated by a delimiter. Here's how we can create a custom collector to do that: ```java Collector<String, StringBuilder, String> stringCollector = Collector.of( StringBuilder::new, (sb, s) -> { if (sb.length() > 0) { sb.append(", "); } sb.append(s); }, (sb1, sb2) -> { if (sb1.length() > 0 && sb2.length() > 0) { sb1.append(", "); } sb1.append(sb2); return sb1; }, StringBuilder::toString ); List<String> words = List.of("apple", "banana", "cherry", "date"); String concatenated = words.stream() .collect(stringCollector); System.out.println(concatenated); // Output: apple, banana, cherry, date ``` In this example, we create a custom collector that takes strings, adds them to a `StringBuilder`, and concatenates them into a single string separated by a comma and a space. The `supplier` method creates a new `StringBuilder`. The `accumulator` method appends each string to the `StringBuilder`, separated by a comma and a space, except for the first string. The `combiner` method combines two `StringBuilders` into one by appending the second `StringBuilder` to the end of the first `StringBuilder`, separated by a comma and a space, except if one of the `StringBuilders` is empty. Finally, the `finisher` method transforms the `StringBuilder` into a `String`. We can use this custom collector to concatenate any stream of strings into a single string separated by a delimiter of our choice. ## Conclusion That's it for this article, ye landlubbers! We've learned how to collect stream elements into various types of collections, including lists, sets, and maps. We've also seen how to create custom collectors when the built-in collectors don't quite fit our needs. Join me next time as we delve deeper into the world of Java Streams and explore their use in parallel processing. Until then, happy coding, and may the wind always be at yer back!

## Custom Collectors for Stream Elements

While the `toList`, `toSet`, and `toMap` methods of the `Collectors` class cover most of our needs for collecting stream elements, sometimes we need more customized behavior. For example, we may want to collect elements into a custom data structure or perform some additional processing on the collected elements.

To achieve this, we can create our own custom collector by implementing the `Collector` interface. The `Collector` interface has four methods that we need to implement: `supplier`, `accumulator`, `combiner`, and `finisher`. These methods specify how to create a new instance of the data structure that will hold the collected elements, how to add an element to the data structure, how to combine two data structures into one, and how to perform any final operations on the data structure before returning it.

Here's an example of a custom collector that collects even numbers into a `LinkedList` and odd numbers into an `ArrayList`:

```java
import java.util.*;
import java.util.stream.Collector;

public class EvenOddCollector implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {

    @Override
    public Supplier<Map<Boolean, List<Integer>>> supplier() {
        return () -> new HashMap<>() {
            put(true, new LinkedList<>());
            put(false, new ArrayList<>());
        };
    }

    @Override
    public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
        return (map, num) -> map.get(num % 2 == 0).add(num);
    }

    @Override
    public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
        return (map1, map2) -> {
            map1.get(true).addAll(map2.get(true));
            map1.get(false).addAll(map2.get(false));
            return map1;
        };
    }

    @Override
    public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.IDENTITY_FINISH);
    }
}

In this example, we implement the Collector interface to create a custom collector that collects even numbers into a LinkedList and odd numbers into an ArrayList. We override the supplier method to create a new instance of a HashMap that contains two lists, one for even numbers and one for odd numbers. We override the accumulator method to add each element to the appropriate list based on whether it is even or odd. We override the combiner method to combine two HashMaps into one. We override the finisher method to return the final HashMap. Finally, we override the characteristics method to specify that this collector doesn’t require a final finishing operation.

Conclusion

And that’s a wrap, me hearties! We’ve learned how to collect stream elements into various types of collections, such as lists, sets, and maps, using the Collectors class. We’ve also explored the creation of custom collectors to achieve more customized behavior. As with any great treasure hoard, the more we know, the more we can collect. So, let’s keep exploring and gathering our bounty with Java Streams!