9

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?

Fureeish
  • 12,533
  • 4
  • 32
  • 62

4 Answers4

8

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]
Ilya Lysenko
  • 1,772
  • 15
  • 24
  • Made a trivial edit for formatting and TreeMap, not that your answer was incorrect though. Its the cleanest with the existing capabilities in my opinion. – Naman Mar 20 '20 at 05:06
  • @Naman, I've rolled it back. Please make a separate answer with that version. Because it uses not only streams. – Ilya Lysenko Mar 20 '20 at 06:17
  • You can take a look again, it does use `Stream` with an initialization of the `TreeMap` using a constructor instead. There is no value add in making another answer for that edit. This still keeps up with OP's expectation and is good with the rollback as well. – Naman Mar 20 '20 at 06:21
  • 1
    Shorter and elegant but this explodes if the `strings` list is empty. – vicpermir Mar 20 '20 at 09:29
7

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]
vicpermir
  • 3,544
  • 3
  • 22
  • 34
  • 1
    The answer from @IlyaLysenko using `TreeMap` is a more elegant version of this. – jaco0646 Mar 19 '20 at 22:34
  • @Fureeish But overly complicated for something as simple as [this answer](https://stackoverflow.com/a/60765407/1746118) or else there is never a harm in defining a custom collector. – Naman Mar 20 '20 at 05:02
6

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);
Naman
  • 27,789
  • 26
  • 218
  • 353
Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • 1
    I do consider correctness as a part of elegance and, in this case, I believe this is not only superior, but surely an eleganet solution. Thank you. I would like to wait for other approaches though, if any appear. If not, I will happily accept your answer! – Fureeish Mar 19 '20 at 21:25
  • You might have meant `list1.get(0).length()` instead of `list1.get(0).size()`. Made an edit, feel free to revert. – Naman Mar 20 '20 at 04:07
4

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());
pero_hero
  • 2,881
  • 3
  • 10
  • 24