11

I often need the maximum element of a collection according to the maximization of a criterion which produces a double or int value. Streams have the max() function which requires me to implement a comparator, which I find cumbersome. Is there a more concise syntax, such as names.stream().argmax(String::length) in the following example?

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class ArgMax
{
    public static void main(String[] args)
    {
        List<String> names = Arrays.asList("John","Joe","Marilyn");
        String longestName = names.stream().max((String s,String t)->(Integer.compare(s.length(),t.length()))).get();
        System.out.println(longestName);
    }
}
Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
Konrad Höffner
  • 11,100
  • 16
  • 60
  • 118
  • 3
    Am I missing something or are none of the solution below actually returning Argmax(). They should return the *index* of the max value, not the value itself. That is a very different problem. https://en.wikipedia.org/wiki/Arg_max – Hephaestus Jul 30 '21 at 19:58

3 Answers3

17

Use

String longestName = names.stream().max(Comparator.comparing(String::length)).get();

to compare elements on some property (can be more complex than that, but doesn't have to).

As Brian suggests in the comments, using Optional#get() like this is unsafe if there's a possibility that the Stream is empty. You'd be better suited to use one of the safer retrieval methods, such as Optional#orElse(Object) which will give you some default value if there is no max.

Community
  • 1
  • 1
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 7
    Gah! Don't use raw `get()` at the end of this, otherwise you will throw NSEE if the stream is empty. Use a safe method like `orElse`, `orElseThrow`, or `ifPresent`. – Brian Goetz Dec 22 '14 at 20:41
  • 5
    @BrianGoetz I think we've discovered one of Brian's allergies. He sneezes whenever I write `Optional.get()` too. – Stuart Marks Dec 23 '14 at 00:36
  • 4
    @StuartMarks Indeed so. We should have never called it `get`. We should have called it `getOrThrow()` or `orElseThrow()` or `getUnsafely()` or probably just `getOrThrowNoSuchElementExcpetionIfThisOptionalIsNotPresent()`. – Brian Goetz Dec 23 '14 at 03:16
  • In this case how do I get the index of longestName value? I assume that `names.indexOf(longestName)` is OK? – zygimantus Jan 10 '16 at 17:08
  • 4
    @zygimantus: if you can live with a second linear search, yes. Otherwise, use `IntStream.range(0, names.length()).boxed().max(Comparator.comparingInt(ix -> names.get().length())` to search for the index in the first place. – Holger Oct 17 '17 at 06:24
4

I think one should consider that while max/min are unique, this is of course not guaranteed for the argMax/argMin; this in particular implies that the type of the reduction should be a collection, such as for instance a List. This requires a bit more work than suggested above.

The following ArgMaxCollector<T> class provides a simple implementation of such a reduction. The main shows an application of such class to compute the argMax/argMin of the set of strings

one two three four five six seven

ordered by their length. The output (reporting the result of the argMax and argMin collectors respectively) should be

[three, seven]
[one, two, six]

that are the two longest and the three shortest strings respectively.

This is my first attempt at using the new Java 8 stream APIs, so any comment will be more than welcome!

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collector;

class ArgMaxCollector<T> {

    private T max = null;
    private ArrayList<T> argMax = new ArrayList<T>();
    private Comparator<? super T> comparator;

    private ArgMaxCollector( Comparator<? super T> comparator ) {
        this.comparator = comparator;
    }

    public void accept( T element ) {
        int cmp = max == null ? -1 : comparator.compare( max, element );
        if ( cmp < 0 ) {
            max = element;
            argMax.clear();
            argMax.add( element );
        } else if ( cmp == 0 )
            argMax.add( element );
    }

    public void combine( ArgMaxCollector<T> other ) {
        int cmp = comparator.compare( max, other.max );
        if ( cmp < 0 ) {
            max = other.max;
            argMax = other.argMax;
        } else if ( cmp == 0 ) {
            argMax.addAll( other.argMax );
        }
    }

    public List<T> get() {
        return argMax;
    }

    public static <T> Collector<T, ArgMaxCollector<T>, List<T>> collector( Comparator<? super T> comparator ) {
        return Collector.of(
            () -> new ArgMaxCollector<T>( comparator ),
            ( a, b ) -> a.accept( b ),
            ( a, b ) ->{ a.combine(b); return a; },
            a -> a.get() 
        );
    }
}

public class ArgMax {

    public static void main( String[] args ) {

        List<String> names = Arrays.asList( new String[] { "one", "two", "three", "four", "five", "six", "seven" } );

        Collector<String, ArgMaxCollector<String>, List<String>> argMax = ArgMaxCollector.collector( Comparator.comparing( String::length ) );
        Collector<String, ArgMaxCollector<String>, List<String>> argMin = ArgMaxCollector.collector( Comparator.comparing( String::length ).reversed() );

        System.out.println( names.stream().collect( argMax ) );
        System.out.println( names.stream().collect( argMin ) );

    }

}
0

Easy efficient solution here:

https://stackoverflow.com/a/63201174/2069400

/** return argmin item, else null if none.  NAN scores are skipped */
public static <T> T argmin(Stream<T> stream, ToDoubleFunction<T> scorer) {
    Double min = null;
    T argmin = null;
    for (T p: (Iterable<T>) stream::iterator) {
        double score = scorer.applyAsDouble(p);
        if (min==null || min > score) {
            min = score;
            argmin = p;
        }
    }
    return argmin;
}
George Forman
  • 600
  • 5
  • 7
  • +1. This minimizes the number of calls to `scorer`. It calls it `n` times. In contrast, the `Stream::min()` calls the `Comparator.comparing` `(n-1)*2` times. Where `n` is number of elements in the stream. This is mighty important, if the comparator is expensive. – Jan Dolejsi Dec 31 '21 at 12:46