42

Is there a concise way to extract both the min and max value of a stream (based on some comparator) in one pass?

There appear to be many ways to get the min and max values individually, or I can sort the stream into a temporary object, for example:

List<T> sorted = Stream.of(...).sorted().collect(Collectors.toList());
T min = sorted.get(0);
T max = sorted.get(sorted.size() - 1);

But this isn't concise and requires allocating a temporary object. I'd rather not allocate a temporary object or make two passes through the stream. Is there an alternative?

Pair<T> extent = Stream.of(...).???
Mzzzzzz
  • 4,770
  • 7
  • 30
  • 47
  • 13
    Have you considered a collector like [IntSummaryStatistics](https://docs.oracle.com/javase/8/docs/api/java/util/IntSummaryStatistics.html)? You may follow the pattern supposing this is not about numbers. – Edwin Dalorzo Jan 23 '17 at 22:07

7 Answers7

69

The summarizingInt collector works well if you have a Stream of Integers.

IntSummaryStatistics stats = Stream.of(2,4,3,2)
      .collect(Collectors.summarizingInt(Integer::intValue));

int min = stats.getMin();
int max = stats.getMax();

If you have doubles you can use the summarizingDouble collector.

DoubleSummaryStatistics stats2 = Stream.of(2.4, 4.3, 3.3, 2.5)
  .collect(Collectors.summarizingDouble((Double::doubleValue)));
Will Humphreys
  • 2,677
  • 3
  • 25
  • 26
28

If this is a frequently needed feature, we better make a Collector to do the job. We'll need a Stats class to hold count, min, max, and factory methods to creat stats collector.

Stats<String> stats = stringStream.collect(Stats.collector())

fooStream.collect(Stats.collector(fooComparator))

(Maybe a better convenience method would be Stats.collect(stream))

I made an example Stats class -

https://gist.github.com/zhong-j-yu/ac5028573c986f7820b25ea2e74ed672

public class Stats<T>
{
    int count;

    final Comparator<? super T> comparator;
    T min;
    T max;

    public Stats(Comparator<? super T> comparator)
    {
        this.comparator = comparator;
    }

    public int count(){ return count; }

    public T min(){ return min; }
    public T max(){ return max; }

    public void accept(T val)
    {
        if(count==0)
            min = max = val;
        else if(comparator.compare(val, min)<0)
            min = val;
        else if(comparator.compare(val, max)>0)
            max = val;

        count++;
    }

    public Stats<T> combine(Stats<T> that)
    {
        if(this.count==0) return that;
        if(that.count==0) return this;

        this.count += that.count;
        if(comparator.compare(that.min, this.min)<0)
            this.min = that.min;
        if(comparator.compare(that.max, this.max)>0)
            this.max = that.max;

        return this;
    }

    public static <T> Collector<T, Stats<T>, Stats<T>> collector(Comparator<? super T> comparator)
    {
        return Collector.of(
            ()->new Stats<>(comparator),
            Stats::accept,
            Stats::combine,
            Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH
        );
    }

    public static <T extends Comparable<? super T>> Collector<T, Stats<T>, Stats<T>> collector()
    {
        return collector(Comparator.naturalOrder());
    }
}
ZhongYu
  • 19,446
  • 5
  • 33
  • 61
  • 1
    I wouldn’t specify `UNORDERED`, as this collector is capable of respecting the encounter order, i.e. provide the first of the maximal/minimal elements if there’s a tie, exactly like `max(…)` and `min(…)` do. – Holger Jan 24 '17 at 10:16
  • 5
    `IntSummaryStatistics` is better – Kelvin Ng Aug 17 '17 at 14:45
10

Map each element of the stream to a pair, where the two elements represent the min and the max; and then reduce the pairs by taking the min of the mins, and the max of the maxes.

For example, using some Pair class and some Comparator<T>:

Comparator<T> comparator = ...;
Optional<Pair<T, T>> minMax = list.stream()
    .map(i -> Pair.of(i /* "min" */, i /* "max" */))
    .reduce((a, b) -> Pair.of(
        // The min of the min elements.
        comparator.compare(a.first, b.first) < 0 ? a.first : b.first,
        // The max of the max elements.
        comparator.compare(a.second, b.second) > 0 ? a.second : b.second));
Andy Turner
  • 137,514
  • 11
  • 162
  • 243
8

starting with Java 12 you can obtain two result or more in a single pass by using Collectors::teeing:

class Movie {
    String title;
    double rating;
    //...
}

class Pair<T1, T2> {
    T1 left;
    T2 right;
    //...
}

@Test
void shouldFindWorstAndBestMovie() {
    var m1 = new Movie("Groundhog Day", 8);
    var m2 = new Movie("Stop! Or My Mom Will Shoot", 4.4);
    var m3 = new Movie("Forrest Gump", 8.8);

    var ratingComparator = Comparator.comparing(Movie::getRating);

    Pair<Movie, Movie> result = Stream.of(m1, m2, m3)
            .collect(Collectors.teeing(
                    Collectors.minBy(ratingComparator),
                    Collectors.maxBy(ratingComparator),
                    (min, max) -> new Pair<>(min.orElse(null), max.orElse(null))
            ));

    assertEquals(m2, result.getLeft(), "min does not match");
    assertEquals(m3, result.getRight(), "max does not match");
}

You can find more details and example in this article.

Adrian
  • 2,984
  • 15
  • 27
7

I think you need that

IntStream myIntStream = IntStream.rangeClosed(1, 100);
IntSummaryStatistics intStatistic = myIntStream.summaryStatistics();

System.out.println("Max: " + intStatistic.getMax() + " Min: " + intStatistic.getMin());
2

For a pure Java solution that's fairly concise, you can use .peek(). This is not truly Functional, as anything that .peek() does is a side-effect. But this does do it all in one pass, doesn't require sorting and isn't too verbose. There is a "temp" Object, the AtomicRef, but you'll probably allocate a local var/ref to hold the min and max anyway.

Comparator<T> cmp = ...
Stream<T> source = ...
final AtomicReference<T> min = new AtomicReference<T>();
Optional<T> max = source.peek(t -> {if (cmp.compare(t,min.get()) < 0) min.set(t);})
    .max(cmp);
//Do whatever with min.get() and max.get()
WillD
  • 875
  • 1
  • 7
  • 14
  • Hm... this relies on `max` having to consume the entire `source`-stream - I'm not sure that is guaranteed in any way (thinking sorted sources, short-circuiting might perhaps be possible?). – Hulk Jan 25 '17 at 13:37
  • The OP was sorting in the original question and want to avoid it. What makes you believe that consuming the stream is not guaranteed? .max(cmp) and .peek() are both defined on the java.util.stream.Stream interface and there's nothing outside of an Exception thrown during pipeline processing that should prevent this... – WillD Jan 25 '17 at 14:29
  • I agree that this approach works with the current version - I was just wondering if it was guaranteed to continue to work in future versions (see for for example [my question regarding `Stream.count`](http://stackoverflow.com/q/41347083/2513200) which will no longer visit all elements in java 9 if it can determine the size in a more efficient way). But with a custom `Comparator`, such an optimization is probably not possible here anyway. – Hulk Jan 26 '17 at 08:07
  • 1
    According to J9 API, `max()` is terminal, but not short-circuiting. I can't imagine an implementation that is short-circuiting outside of taking the `max()` of a sorted Stream (which the OP wanted to avoid). – WillD Jan 31 '17 at 15:37
1

A straightforward approach using any mutable Pair class:

final Pair<T, T> pair = new Pair<>();
final Comparator<T> comparator = ...;
Stream.of(...).forEachOrdered(e -> {
    if(pair.first == null || comparator.compare(e, pair.first) < 0){
        pair.first = e;
    }
    if(pair.second == null || comparator.compare(e, pair.second) > 0){
        pair.second = e;
    }
});
Calculator
  • 2,769
  • 1
  • 13
  • 18