rogger2016
rogger2016

Reputation: 939

Java 8 groupingby Into map that contains a list

I have the following data:

List<Map<String, Object>> products = new ArrayList<>();

Map<String, Object> product1 = new HashMap<>();
product1.put("Id", 1);
product1.put("number", "123");
product1.put("location", "ny");

Map<String, Object> product2 = new HashMap<>();
product2.put("Id", 1);
product2.put("number", "456");
product2.put("location", "ny");

Map<String, Object> product3 = new HashMap<>();
product3.put("Id", 2);
product3.put("number", "789");
product3.put("location", "ny");

products.add(product1);
products.add(product2);
products.add(product3);

I'm trying to stream over the products list, group by the id and for each id have a list on number, while returning a Map that contains three keys: Id, List of number, and a location.

So my output would be:

List<Map<String, Object>>> groupedProducts
map[0]
    {id:1, number[123,456], location:ny}
map[1]
    {id:2, number[789], location:ny}

I have tried:

Map<String, List<Object>> groupedProducts = products.stream()
      .flatMap(m -> m.entrySet().stream())
      .collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toList())));

which prints:

{number=[123, 456, 789], location=[ny, ny, ny], Id=[1, 1, 2]}

I realise Map<String, List<Object>> is incorrect, but it's the best I could achieve to get the stream to work. Any feedback is appreciated.

Upvotes: 2

Views: 1863

Answers (1)

Szymon Stepniak
Szymon Stepniak

Reputation: 42184

In your case grouping by Id key with Collectors.collectingAndThen(downstream, finisher) could do the trick. Consider following example:

Collection<Map<String, Object>> finalMaps = products.stream()
        .collect(groupingBy(it -> it.get("Id"), Collectors.collectingAndThen(
                Collectors.toList(),
                maps -> (Map<String, Object>) maps.stream()
                        .reduce(new HashMap<>(), (result, map) -> {
                            final List<Object> numbers = (List<Object>) result.getOrDefault("number", new ArrayList<>());

                            result.put("Id", map.getOrDefault("Id", result.getOrDefault("Id", null)));
                            result.put("location", map.getOrDefault("location", result.getOrDefault("location", null)));

                            if (map.containsKey("number")) {
                                numbers.add(map.get("number"));
                            }
                            result.put("number", numbers);

                            return result;
                        }))
                )
        )
        .values();

System.out.println(finalMaps);

In the first step you group all maps with the same Id value to a List<Map<String,Object>> (this is what Collectors.toList() passed to .collectingAndThen() does). After creating that list "finisher" function is called - in this case we transform list of maps into a single map using Stream.reduce() operation - we start with an empty HashMap<String,Object> and we iterate over maps, take values from current map in iteration and we set values according to your specification ("Id" and "location" gets overridden, "number" keeps a list of values).

Output

[{number=[123, 456], location=ny, Id=1}, {number=[789], location=ny, Id=2}]

To make code more simple you can extract BiOperator passed to Stream.reduce to a method and use method reference instead. This function defines what does it mean to combine two maps into single one, so it is the core logic of the whole reduction.

Upvotes: 2

Related Questions