How to create a nested map using Collectors.groupingBy?

I have a list of classes 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 shown below: -

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

Properties should be displayed as follows:

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

I know that one level of grouping can be easily done without any problems using Collectors.groupingBy .

I tried to use this for a nested level, but I did not succeed when the same values ​​started for fields that are keys.

My code looks something like this: -

  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 someone knows a good approach to this using threads, please share!

+7
java java-8 java-stream
source share
1 answer

Abstract / Brief Discussion

The presence of a map of map maps is doubtful when viewed from an object-oriented view, as it might seem like you are lacking in abstraction (i.e. you could create a Result class that encapsulates the results of a nested grouping). However, this is perfectly reasonable when viewed solely from a pure data-driven approach.

So, here I present two approaches: the first is purely data-oriented (with nested groupingBy calls, therefore, nested maps), and the second is more OO-friendly and does a better job of abstracting grouping criteria. Just choose the one that better reflects your the intentions and standards / traditions of coding and, more importantly, the one you like best.


Data Oriented Approach

For the first approach, you can simply 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 can see, the result is Map<String, Map<String, Map<String, List<Booker>>>> . This is because there can be more than one ProductDto instance with the same combination (status, category, type) .

Also, since you need Booker instances instead of ProductDto instances, I adapt the last groupingBy collector to return Booker instead of ProductDto s.


About abbreviation

If you need only one Booker instance instead of List<Booker> as the value of the innermost map, you will need a way to reduce Booker instances, that is, convert many instances to one using an associative operation (accumulating the sum of some attribute is the most common).


Object oriented approach

For the second approach, the presence of Map<String, Map<String, Map<String, List<Booker>>>> can be considered as bad practice or even as pure evil. Thus, instead of having a list card map card, you can only have one list card, whose keys represent a combination of the three properties that you want to group.

The easiest way to do this is to use List as a key, since 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 use Java 9+, you can use List.of instead of Arrays.asList , as List.of returns a completely immutable and optimized list.

+12
source share

All Articles