How can I filter stream by some max value from it?

Viewed 342

Let's say I have a stream of following object.

class MyObject {
    private Some some; // Comparable
    private Other value
}

Is there any idiom doing following things in a single chain?

  • With given Stream<MyObject>,
  • Finds the maximum value of some.
  • Map values whose some is the maximum.

as if,

final List<MyObject> list = getList();
final Some max = list.stream().max(Comparator.naturalOrder())
final List<Other> list = list.stream()
        .filter(e -> Objects.equals(e.some, max)
        .map(e -> e.getValue()).collect(toList());
2 Answers

If you have to do in one pass, you can write a custom Collector to reduce the stream into a list of max elements. Here's an implementation based on this answer by Stuart Marks.

List<MyObject> maxList = list.stream()
                             .collect(maxList(Comparator.comparing(MyObject::getSome)));

static <T> Collector<T,?,List<T>> maxList(Comparator<? super T> comp) {
    return Collector.of(
        ArrayList::new,
        (list, t) -> {
            int c;
            if (list.isEmpty() || (c = comp.compare(t, list.get(0))) == 0) {
                list.add(t);
            } else if (c > 0) {
                list.clear();
                list.add(t);
            }
        },
        (list1, list2) -> {
            if (list1.isEmpty()) {
                return list2;
            } 
            if (list2.isEmpty()) {
                return list1;
            }
            int r = comp.compare(list1.get(0), list2.get(0));
            if (r < 0) {
                return list2;
            } else if (r > 0) {
                return list1;
            } else {
                list1.addAll(list2);
                return list1;
            }
        });
}

The Collector will maintain an ArrayList to be used for the result, and accumulate each element into it, checking how the element compares to the first element of the current list. The part c = comp.compare(t, list.get(0))) == 0 will check if the element has the same max value, and if so will add it to the list.

If you're on Java 12+, you can use Collectors.teeing:

List<Other> maxObjects = list.stream().collect(
    Collectors.teeing(
        Collectors.maxBy(Comparator.comparing(MyObject::getSome)),
        Collectors.groupingBy(
            MyObject::getSome,
            Collectors.mapping(
                MyObject::getValue, 
                Collectors.toList())),
        (max, groups) -> max.map(MyObject::getSome).map(groups::get).orElse(null)));
Related