18

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.

Community
  • 1
  • 1
James Murphy
  • 800
  • 1
  • 15
  • 29
  • From the example given in https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#groupingBy-java.util.function.Function-java.util.function.Supplier-java.util.stream.Collector- it looks like it should compile. Possibly `Collectors.toList()` instead of `toList()`, unless you imported it statically? – njzk2 Oct 15 '15 at 13:48
  • @njzk2 I don't think the toList is the problem - that's statically imported as you say. The LinkedHashMap::new line has the following error in IntelliJ: Bad return type in method reference: cannot convert java.util.LinkedHashMap to M – James Murphy Oct 15 '15 at 13:50
  • 1
    the mapping collectors apply to the values so you want p -> p instead of PlaceSummary::getPlaceId – Alexis C. Oct 15 '15 at 13:51
  • @AlexisC. so that would be p -> p.getPlaceId ? so... does that mean I can just use p -> p to refer to "this" element? – James Murphy Oct 15 '15 at 13:52
  • 1
    No p -> p.getPlaceId means that the PlaceSummary grouped values will be mapped by their place id. If you want the instances themselves, just apply the identity function so p -> p. – Alexis C. Oct 15 '15 at 13:57
  • `PlaceSummary::getPlaceId` in the groupingBy is weird, it returns apparently a String, but you want a `PlaceSummary` - Edit, didn't see the answer – njzk2 Oct 15 '15 at 14:40

4 Answers4

23

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);
Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • Tried it out in production code - works like a dream thank you. Definitely a misunderstanding of the API. – James Murphy Oct 15 '15 at 14:57
  • In addition, I preferred placeSummary -> placeSummary to Function.identity() since it seems a bit clearer To clarify as well. The code isn't too wierd if you understand the purpose of it. What I was attempting to do was group duplicate Ids but then filter out duplicates using rules. One of those rules being to keep one of the duplicates based on whether it's an augmented place (we augment places to give them additional information that the Google API doesn't know) or not. – James Murphy Oct 15 '15 at 14:58
4

I think you got a little confused about the final collector. It merely represents what needs to be in each map value. There is no need to have a secondary mapping collector, as you just want a list of the original objects.

    Map<String, List<PlaceSummary>> placesGroupedByPlaceId =
          places.stream()
                .collect(Collectors.groupingBy(PlaceSummary::getPlaceId,
                                               LinkedHashMap::new,
                                               Collectors.toList()));
RealSkeptic
  • 33,993
  • 7
  • 53
  • 79
  • Yeah I think that's what confused me a little. I think it would have been preferable to have the argument orders key, value mapping, toList() if I'm honest - but small gripe. Will remember this for future reference... thanks. – James Murphy Oct 15 '15 at 14:03
0
/**
 * I have written this code more generic, if you want then you can group based on any * 
 * instance variable , id, name etc via passing method reference.
**/

class Student {
    private int id;
    private String name;
    public Student(int id, String name) {this.id = id;this.name = name;}
    /**
     * @return the id
     */
    public int getId() {return id;}
    /**
     * @param id
     *            the id to set
     */
    public void setId(int id) {this.id = id;}
    /**
     * @return the name
     */
    public String getName() {return name;}
    /**
     * @param name
     *            the name to set
     */
    public void setName(String name) {this.name = name;}
}

public class StudentMain {

    public static void main(String[] args) {

        List<Student> list = new ArrayList<>();
        list.add(new Student(1, "Amit"));
        list.add(new Student(2, "Sumit"));
        list.add(new Student(1, "Ram"));
        list.add(new Student(2, "Shyam"));
        list.add(new Student(3, "Amit"));
        list.add(new Student(4, "Pankaj"));

        Map<?, List<Student>> studentById = groupByStudentId(list,
                Student::getId);
        System.out.println(studentById);

       Map<?, List<Student>> studentByName = groupByStudentId(list,
                Student::getName);
        System.out.println(studentByName);

    }

    private static <K, V> Map<?, List<V>> groupByStudentId(List<V> list,
            Function<V, K> keyFunction) {
        return list.stream().collect(
                Collectors.groupingBy(keyFunction, HashMap::new,
                        Collectors.toList()));
    }
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Ram Dular
  • 1
  • 1
0

If you need a grouping while mantaining the order and apply a function(Reduction) perhaps counting i use something like this.

final Map<Integer,Long>map=stream.collect(Collectors.groupingBy(function
   ,LinkedHashMap::new
   ,Collectors.collectingAndThen(Collectors.counting(),Function.identity()))
 )
chiperortiz
  • 4,751
  • 9
  • 45
  • 79