45

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(...).??? 
1
  • 14
    Have you considered a collector like IntSummaryStatistics? You may follow the pattern supposing this is not about numbers. Commented Jan 23, 2017 at 22:07

8 Answers 8

73

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))); 
Sign up to request clarification or add additional context in comments.

Comments

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()); } } 

2 Comments

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.
IntSummaryStatistics is better
14

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"); } 

Comments

12

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)); 

5 Comments

Not quite as concise as I was hoping for, but this looks good. Would be nice if there were a Comparator.min() and Comparator.max() to simplify the last two lines.
Is there a Pair in Guava?
Guava doesn't have a Pair.
Did you mean Apache Commons?
Oops. Well, the implementation isn't especially relevant. I'll just leave it at "some Pair".
8

I think you need that

IntStream myIntStream = IntStream.rangeClosed(1, 100); IntSummaryStatistics intStatistic = myIntStream.summaryStatistics(); System.out.println("Max: " + intStatistic.getMax() + " Min: " + intStatistic.getMin()); 

Comments

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() 

4 Comments

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?).
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...
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 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.
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).
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; } }); 

Comments

-1
List<Integer> list = Arrays.asList(1,5,8,3,6); System.out.println(list.stream().count());//count System.out.println(list.stream().min((i1,i2) -> i1.compareTo(i2)).stream() .findFirst().get()); //min element System.out.println(list.stream().max((i1,i2) -> i1.compareTo(i2)).stream() .findFirst().get()); // max element 

1 Comment

request from the question: "in one pass"

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.