Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you group elements in a List<P> to a Map<K, List<V>> while retaining order?

I have a List of Google PlaceSummary objects taken from the Google Places API. I'd like to collect and group them by their Google Place ID, but also retain the order of the elements. What I thought would work would be:

Map<String, List<PlaceSummary>> placesGroupedByPlaceId =
            places.stream()
                  .collect(Collectors.groupingBy(
                          PlaceSummary::getPlaceId,
                          LinkedHashMap::new,
                          Collectors.mapping(PlaceSummary::getPlaceId, toList())
                  ));

But it won't even compile. It looks like it should according to the Java API documentation on Collectors.

Previously I had this code:

    Map<String, List<PlaceSummary>> placesGroupedByPlaceId = places.stream()
            .collect(Collectors.groupingBy(PlaceSummary::getPlaceId));

However standard .collect() on the Streams API does not retain the order of elements in the subsequent HashMap (obviously since HashMaps are unordered). I wish for the output to be a LinkedHashMap so that the Map is sorted by the insertion order of each bucket.

However, the solution I suggested doesn't compile. Firstly, it doesn't recognise the PlaceSummary::getPlaceId since it says it's not a function - even though I know it is. Secondly, it says I cannot convert LinkedHashMap<Object, Object> into M. M is supposed to be a generic collection, so it should be accepted.

How do I convert the List into a LinkedHashMap using the Java Stream API? Is there a succinct way to do it? If it's too difficult to understand I may just resort to old school pre-Java 8 methods.

I noticed that there is another Stack Overflow answer on converting List to LinkedHashMap, but this doesn't have a solution I want as I need to collect 'this' the object I'm specifically iterating over.

like image 355
James Murphy Avatar asked Oct 15 '15 13:10

James Murphy


1 Answers

You're really close to what you want:

Map<String, List<PlaceSummary>> placesGroupedByPlaceId =
            places.stream()
                  .collect(Collectors.groupingBy(
                          PlaceSummary::getPlaceId,
                          LinkedHashMap::new,
                          Collectors.mapping(Function.identity(), Collectors.toList())
                  ));

In the Collectors.mapping method, you need to give the PlaceSummary instance and not the place ID. In the code above, I used Function.identity(): this collector is used to build the values so we need to accumulate the places themselves (and not their ID).

Note that it is possible to write directly Collectors.toList() instead of Collectors.mapping(Function.identity(), Collectors.toList()).

The code you have so far does not compile because it is in fact creating a Map<String, List<String>>: you are accumulating the IDs for each ID (which is quite weird).


You could write this as a generic method:

private static <K, V> Map<K, List<V>> groupByOrdered(List<V> list, Function<V, K> keyFunction) {
    return list.stream()
                .collect(Collectors.groupingBy(
                    keyFunction,
                    LinkedHashMap::new,
                    Collectors.toList()
                ));
}

and use it like this:

Map<String, List<PlaceSummary>> placesGroupedById = groupByOrdered(places, PlaceSummary::getPlaceId);
like image 166
Tunaki Avatar answered Oct 15 '22 14:10

Tunaki