Amitoj
Amitoj

Reputation: 433

Collect results of a map operation in a Map using Collectors.toMap or groupingBy

I've got a list of type List<A> and with map operation getting a collective list of type List<B> for all A elements merged in one list.

List<A> listofA = [A1, A2, A3, A4, A5, ...]

List<B> listofB = listofA.stream()
  .map(a -> repo.getListofB(a))
  .flatMap(Collection::stream)
  .collect(Collectors.toList());

without flatmap

List<List<B>> listOflistofB = listofA.stream()
  .map(a -> repo.getListofB(a))
  .collect(Collectors.toList());

I want to collect the results as a map of type Map<A, List<B>> and so far trying with various Collectors.toMap or Collectors.groupingBy options but not able to get the desired result.

Upvotes: 7

Views: 2060

Answers (4)

Ravindra Ranwala
Ravindra Ranwala

Reputation: 21124

You can use the toMap collector with a bounded method reference to get what you need. Also notice that this solution assumes you don't have repeated A instances in your source container. If that precondition holds this solution would give you the desired result. Here's how it looks.

Map<A, Collection<B>> resultMap = listofA.stream()
    .collect(Collectors.toMap(Function.identity(), repo::getListofB);

If you have duplicate A elements, then you have to use this merge function in addition to what is given above. The merge function deals with key conflicts if any.

Map<A, Collection<B>> resultMap = listofA.stream()
       .collect(Collectors.toMap(Function.identity(), repo::getListofB, 
            (a, b) -> {
                a.addAll(b);
                return a;
        }));

And here's a much more succinct Java9 approach which uses the flatMapping collector to handle repeated A elements.

Map<A, List<B>> aToBmap = listofA.stream()
        .collect(Collectors.groupingBy(Function.identity(),
                Collectors.flatMapping(a -> getListofB(a).stream(), 
                        Collectors.toList())));

Upvotes: 7

fps
fps

Reputation: 34470

In this answer, I'm showing what happens if you have repeated A elements in your List<A> listofA list.

Actually, if there were duplicates in listofA, the following code would throw an IllegalStateException:

Map<A, Collection<B>> resultMap = listofA.stream()
        .collect(Collectors.toMap(
                            Function.identity(), 
                            repo::getListofB);

The exception might be thrown because Collectors.toMap doesn't know how to merge values when there is a collision in the keys (i.e. when the key mapper function returns duplicates, as it would be the case for Function.identity() if there were repeated elements in the listofA list).

This is clearly stated in the docs:

If the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.

The docs also give us the solution: in case there are repeated elements, we need to provide a way to merge values. Here's one such way:

Map<A, Collection<B>> resultMap = listofA.stream()
        .collect(Collectors.toMap(
                            Function.identity(), 
                            a -> new ArrayList<>(repo.getListofB(a)),
                            (left, right) -> {
                                left.addAll(right);
                                return left;
                            });

This uses the overloaded version of Collectors.toMap that accepts a merge function as its third argument. Within the merge function, Collection.addAll is being used to add the B elements of each repeated A element into a unqiue list for each A.

In the value mapper function, a new ArrayList is created, so that the original List<B> of each A is not mutated. Also, as we're creating an Arraylist, we know in advance that it can be mutated (i.e. we can add elements to it later, in case there are duplicates in listofA).

Upvotes: 2

kgautron
kgautron

Reputation: 8283

To collect a Map where keys are the A objects unchanged, and the values are the list of corresponding B objects, you can replace the toList() collector by the following collector :

toMap(Function.identity(), a -> repo.getListOfB(a))

The first argument defines how to compute the key from the original object : identity() takes the original object of the stream unchanged.

The second argument defines how the value is computed, so here it just consists of a call to your method that transforms a A to a list of B.

Since the repo method takes only one parameter, you can also improve clarity by replacing the lambda with a method reference :

toMap(Function.identity(), repo::getListOfB)

Upvotes: 1

Vikas
Vikas

Reputation: 7185

It would be straight forward,

listofA.stream().collect(toMap(Function.identity(), a -> getListofB(a)));

Upvotes: 2

Related Questions