Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a nested Map using Collectors.groupingBy?

I have a list of class say ProductDto

public class ProductDto {
    private String Id;
    private String status;
    private Booker booker;
    private String category;
    private String type;
}

I want to have a Map as below:-

Map<String,Map<String,Map<String,Booker>>

The properties are to be mapped as below:

Map<status,Map<category,Map<type,Booker>

I know one level of grouping could be done easily without any hassles using Collectors.groupingBy.

I tried to use this for nested level but it failed for me when same values started coming for fields that are keys.

My code is something like below:-

 list.stream()
                .collect(Collectors.groupingBy(
                        (FenergoProductDto productDto) -> 
                        productDto.getStatus()
                        ,
                        Collectors.toMap(k -> k.getProductCategory(), fProductDto -> {
                            Map<String, Booker> productTypeMap = new ProductTypes();
                            productTypeMap.put(fProductDto.getProductTypeName(),
                                    createBooker(fProductDto.getBookingEntityName()));
                            return productTypeMap;
                        })
                ));

If anyone knows a good approach to do this by using streams, please share!

like image 359
Vinay Prajapati Avatar asked Feb 07 '18 13:02

Vinay Prajapati


1 Answers

Abstract / Brief discussion

Having a map of maps of maps is questionable when seen from an object-oriented prespective, as it might seem that you're lacking some abstraction (i.e. you could create a class Result that encapsulates the results of the nested grouping). However, it's perfectly reasonable when considered exclusively from a pure data-oriented approach.

So here I present two approaches: the first one is purely data-oriented (with nested groupingBy calls, hence nested maps), while the second one is more OO-friendly and makes a better job at abstracting the grouping criteria. Just pick the one which better represents your intentions and coding standards/traditions and, more importantly, the one you most like.


Data-oriented approach

For the first approach, you can just nest the groupingBy calls:

Map<String, Map<String, Map<String, List<Booker>>>> result = list.stream()
    .collect(Collectors.groupingBy(ProductDto::getStatus,
             Collectors.groupingBy(ProductDto::getCategory,
             Collectors.groupingBy(ProductDto::getType,
                 Collectors.mapping(
                         ProductDto::getBooker,
                         Collectors.toList())))));

As you see, the result is a Map<String, Map<String, Map<String, List<Booker>>>>. This is because there might be more than one ProductDto instance with the same (status, category, type) combination.

Also, as you need Booker instances instead of ProductDto instances, I'm adapting the last groupingBy collector so that it returns Bookers instead of productDtos.


About reduction

If you need to have only one Booker instance instead of a List<Booker> as the value of the innermost map, you would need a way to reduce Booker instances, i.e. convert many instances into one by means of an associative operation (accumulating the sum of some attribute being the most common one).


Object-oriented friendly approach

For the second approach, having a Map<String, Map<String, Map<String, List<Booker>>>> might be seen as bad practice or even as pure evil. So, instead of having a map of maps of maps of lists, you could have only one map of lists whose keys represent the combination of the 3 properties you want to group by.

The easiest way to do this is to use a List as the key, as lists already provide hashCode and equals implementations:

Map<List<String>, List<Booker>> result = list.stream()
    .collect(Collectors.groupingBy(
         dto -> Arrays.asList(dto.getStatus(), dto.getCategory(), dto.getType()),
         Collectors.mapping(
                 ProductDto::getBooker,
                 Collectors.toList())))));

If you are on Java 9+, you can use List.of instead of Arrays.asList, as List.of returns a fully immutable and highly optimized list.

like image 140
fps Avatar answered Sep 18 '22 15:09

fps