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

James Murphy picture James Murphy · Oct 15, 2015 · Viewed 7.5k times · Source

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.

Answer

Tunaki picture Tunaki · Oct 15, 2015

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);