1. Overview

In this tutorial, we’re going to convert a List into a Map<K, List>. We’ll achieve this with Java’s Stream API and the Supplier functional interface.

2. Supplier in JDK 8

Supplier is often used as a factory. A method can take a Supplier as input and constrains the type using a bounded wildcard type, then the client can pass in a factory that creates any subtype of the given type.

Besides that, the Supplier can perform a lazy generation of values.

3. Converting the List to Map

The Stream API provides support for List manipulation. One such example is the Stream#collect method. However, there isn’t a way in the Stream API methods to give Suppliers to the downstream parameters directly.

In this tutorial, we’ll take a look at the Collectors.groupingBy, Collectors.toMap, and Stream.collect methods with example code snippets. We’ll focus on methods that allow us to use a custom Supplier.

In this tutorial, we’ll process a String List collections in the following examples:

List source = Arrays.asList("List", "Map", "Set", "Tree");

We’ll aggregate the above list into a map whose key is the string’s length. When we’re done, we’ll have a map that looks like:

{
    3: ["Map", "Set"],
    4: ["List", "Tree"]
}

3.1. Collectors.groupingBy()

With Collectors.groupingBy, we can convert a Collection to a Map with a specific classifier. The classifier is an element’s attribute, we’ll use this attribute to incorporate the elements into different groups:

public Map<Integer, List> groupingByStringLength(List source, 
    Supplier<Map<Integer, List>> mapSupplier, 
    Supplier<List> listSupplier) {
    return source.stream()
        .collect(Collectors.groupingBy(String::length, mapSupplier, Collectors.toCollection(listSupplier)));
}

We can validate it works with:

Map<Integer, List> convertedMap = converter.groupingByStringLength(source, HashMap::new, ArrayList::new);
assertTrue(convertedMap.get(3).contains("Map"));

3.2. Collectors.toMap()

The Collectors.toMap method reduces the elements within a stream into a Map.

We start by defining the method with source string and both List and Map suppliers:

public Map<Integer, List> collectorToMapByStringLength(List source, 
        Supplier<Map<Integer, List>> mapSupplier, 
        Supplier<List> listSupplier)

We then define how to obtain the key and the value out of an element. For that we make use of two new functions:

Function<String, Integer> keyMapper = String::length;

Function<String, List> valueMapper = (element) -> {
    List collection = listSupplier.get();
    collection.add(element);
    return collection;
};

Finally, we define a function that is called upon key conflict. In this case, we want to combine the contents of both:

BinaryOperator<List> mergeFunction = (existing, replacement) -> {
    existing.addAll(replacement);
    return existing;
};

Putting everything together, we get:

source.stream().collect(Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapSupplier))

Note that most of the time the functions we define are anonymous inline functions inside the argument list of the method.

Let’s test it:

Map<Integer, List> convertedMap = converter.collectorToMapByStringLength(source, HashMap::new, ArrayList::new);
assertTrue(convertedMap.get(3).contains("Map"));

3.3. Stream.collect()

The Stream.collect method can be used to reduce the elements in a stream into a Collection of any type.

For that, we also need to define a method with both List and Map suppliers that will be called once a new collection is needed:

public Map<Integer, List> streamCollectByStringLength(List source, 
        Supplier<Map<Integer, List>> mapSupplier, 
        Supplier<List> listSupplier)

We then move to define an accumulator that, given the key to the element, gets an existing list, or creates a new one, and adds the element to the response:

BiConsumer<Map<Integer, List>, String> accumulator = (response, element) -> {
    Integer key = element.length();
    List values = response.getOrDefault(key, listSupplier.get());
    values.add(element);
    response.put(key, values);
};

We finally move to combine the values generated by the accumulator function:

BiConsumer<Map<Integer, List>, Map<Integer, List>> combiner = (res1, res2) -> {
    res1.putAll(res2);
};

Putting everything together, we then just call the collect method on the stream of our elements:

source.stream().collect(mapSupplier, accumulator, combiner);

Note that most of the time the functions we define are anonymous inline functions inside the argument list of the method.

The test result will be the same as the other two methods:

Map<Integer, List> convertedMap = converter.streamCollectByStringLength(source, HashMap::new, ArrayList::new);
assertTrue(convertedMap.get(3).contains("Map"));

4. Conclusion

In this tutorial, we illustrated how to convert a List into a Map<K, List> with the Java 8 Stream API with custom Suppliers.

The complete source code with the examples in this tutorial can be found over on GitHub.