13

I want to take a set of objects (ObjectInstance in this case), and I want to group them by one property, and have the resulting lists be sorted on another.

Set<ObjectInstance> beans = server.queryMBeans(null, null);
Map<String, List<String>> beansByDomain = beans.stream()
            .collect(groupingBy( (ObjectInstance oi) -> oi.getObjectName().getDomain(),
                                mapping((ObjectInstance oi) -> oi.getObjectName().getCanonicalKeyPropertyListString(),
                                toList() )));

The above expression creates the correct data structure: a Map where the keys are the domains of the ObjectInstance objects, and the values are Lists of the property lists. What I want is to now sort the Lists, to make sure they are in alphabetical order. Is there some way to do this in the same expression?

One idea would be to add .sort() right after .stream(), but is that really guaranteed to work?

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
Ken DeLong
  • 341
  • 2
  • 5
  • 16

2 Answers2

16

Use collectingAndThen:

List<String> beansByDomain = beans.stream()
        .collect(groupingBy( (ObjectInstance oi) -> oi.getObjectName().getDomain(),
                            mapping((ObjectInstance oi) -> oi.getObjectName().getCanonicalKeyPropertyListString(),
                            collectingAndThen(toList(), (l -> l.stream().sorted().collect(toList()))) )));

You can extract the Collector to make the code more readable:

public static <T> Collector<T,?,List<T>> toSortedList() {
    return Collectors.collectingAndThen(Collectors.toList(), 
                                       l -> l.stream().sorted().collect(toList()));
}

 List<String> beansByDomain = beans.stream()
        .collect(groupingBy( (ObjectInstance oi) -> oi.getObjectName().getDomain(),
                            mapping((ObjectInstance oi) -> oi.getObjectName().getCanonicalKeyPropertyListString(),
                                    toSortedList())));
Jean Logeart
  • 52,687
  • 11
  • 83
  • 118
5

Surely you can sort the whole stream before collecting:

Map<String, List<String>> beansByDomain = beans.stream()
        .map(ObjectInstance::getObjectName)
        .sorted(Comparator.comparing(ObjectName::getCanonicalKeyPropertyListString))
        .collect(groupingBy(ObjectName::getDomain,
                            mapping(ObjectName::getCanonicalKeyPropertyListString,
                            toList() )));

Note that I added the .map(ObjectInstance::getObjectName) step as you don't need anything else from ObjectInstance. This will work nicely, though I cannot predict whether it's faster than sorting each resulting list separately or not.

If you prefer the separate toSortingList() collector (as in @JeanLogeart answer), it can be optimized like this:

public static <T extends Comparable<T>> Collector<T,?,List<T>> toSortedList() {
    return collectingAndThen(toCollection(ArrayList::new),
                    (List<T> l) -> {l.sort(Comparator.naturalOrder()); return l;});
}

Here we explicitly collect to ArrayList (toList() does the same, but it's not guaranteed), then sort the resulting list in-place without additional copying (using stream().sorted().collect(toList()) you copy the whole list content at least twice). Also note that <T> parameter must be declared as extends Comparable<T>. Otherwise you can mistakenly use this collector for non-comparable type which would compile fine, but result in runtime error.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • I like this solution too. I should have thought of the mapping to ObjectName, it makes everything cleaner. Yesterday was a long day... – Ken DeLong Aug 24 '15 at 16:08