How can I collect only the elements of the greatest length with Java Streams?

Viewed 540

I am trying to use Java Streams to collects all the Strings of the greatest length from my list:

List<String> strings = Arrays.asList("long word", "short", "long wwww", "llll wwww", "shr");

List<String> longest = strings.stream()
            .sorted(Comparator.comparingInt(String::length).reversed())
            .takeWhile(???)
            .collect(Collectors.toList());

I would like my longest to contain {"long word", "long wwww", "llll wwww"}, because those are the Strings that have the greatest lengths. In case of only having one of the Strings with greatest length, I am obviously expecting the resulting List to contain only that element.

I tried to first sort them in order to have the greatest length appear in the first element, but I am unable to retrieve the length of the first element in the stream. I could try something like peek():

static class IntWrapper {
    int value;
}

public static void main(String[] args) throws IOException {
    List<String> strings = Arrays.asList("long word", "short", "long wwww", "llll wwww", "shr");

    IntWrapper wrapper = new IntWrapper();

    List<String> longest = strings.stream()
            .sorted(Comparator.comparingInt(String::length).reversed())
            .peek(s -> {
                if (wrapper.value < s.length()) wrapper.value = s.length();
            })
            .takeWhile(s -> s.length() == wrapper.value)
            .collect(Collectors.toList());

    System.out.println(longest);
}

but it's... ugly? I don't like the introduction of dummy wrapper (thank you, effectively final requirement) or the peek() hack.

Is there any more elegant way to achieve this?

4 Answers

Try this:

List<String> strings = Arrays.asList("long word", "short", "long wwww", "llll wwww", "shr");

List<String> longest = strings.stream()
        .collect(groupingBy(String::length, TreeMap::new, toList()))
        .lastEntry()
        .getValue();

System.out.println(longest);

Output:

[long word, long wwww, llll wwww]

Well, I don't know if this will be more elegant but it should do what you want:

List<String> strings = Arrays.asList("long word", "short", "long wwww", "llll wwww", "shr");

List<String> longest = strings.stream()
        .collect(Collectors.groupingBy(String::length))     // Build Map<Length, List<Strings>>
        .entrySet().stream()                                // EntrySet stream of said map
        .max(Map.Entry.comparingByKey())                    // Keep max length
        .map(Map.Entry::getValue)                           // Get value of max length
        .orElse(Collections.emptyList());                   // Or return an empty list if there's none

System.out.println(longest);

Output:

[long word, long wwww, llll wwww]

You may consider it uglier, but a custom collector is definitely correct, more efficient, and even parallelizes nicely:

Collector<String, List<String>, List<String>> collector = Collector.of(
   ArrayList::new,
   (list, elem) -> {
     if (list.isEmpty() || elem.length() == list.get(0).length()) {
       list.add(elem);
     } else if (elem.length() > list.get(0).length()) {
       list.clear();
       list.add(elem);
     }
   },
   (list1, list2) -> {
     int len1 = list1.isEmpty() ? -1 : list1.get(0).length();
     int len2 = list2.isEmpty() ? -1 : list2.get(0).length();
     if (len1 < len2) {
       return list2;
     } else if (len1 > len2) {
       return list1;
     } else {
       list1.addAll(list2);
       return list1;
     }
   });

return strings.stream().collect(collector);

I don't know if you find it more elegant, but it is succinct:

       List<String> strings = Arrays.asList("long word", "short", "long wwww", "llll wwww", "shr");

       TreeMap<Integer, List<String>> collect = strings.stream().collect(
                Collectors.groupingBy(
                        String::length,
                        TreeMap::new,
                        mapping(Function.identity(), toList())));

       System.out.println(collect.lastEntry().getValue());
Related