Say I have a list with elements (34, 11, 98, 56, 43)
.
Using Java 8 streams, how do I find the index of the minimum element of the list (e.g. 1 in this case)
You could do it like this:
int indexMin = IntStream.range(0, list.size())
.mapToObj(i -> new SimpleEntry<>(i, list.get(i)))
.min(comparingInt(SimpleEntry::getValue))
.map(SimpleEntry::getKey)
.orElse(-1);
If the list is a random access list, get
is a constant time operation. The API lacks of a standard tuple class, so I used the SimpleEntry
from the AbstractMap
class as a substitute.
So IntStream.range
generates a stream of indexes from the list from which you map each index to its corresponding value. Then you get the minimum element by providing a comparator on the values (the ones in the list). From there you map the Optional<SimpleEntry<Integer, Integer>>
to an Optional<Integer>
from which you get the index (or -1 if the optional is empty).
As an aside, I would probably use a simple for-loop to get the index of the minimum value, as your combination of min
/ indexOf
does 2 passes over the list.
You might also be interested to check Zipping streams using JDK8 with lambda (java.util.stream.Streams.zip)
Since this is for learning purposes, let's try to find a solution that doesn't just somehow use a stream, but actually works on the stream of our list. We also don't want to assume random access.
So, there are two ways to get a non-trivial result out of a stream: collect
and reduce
. Here is a solution that uses a collector:
class Minimum {
int index = -1;
int range = 0;
int value;
public void accept(int value) {
if (range == 0 || value < this.value) {
index = range;
this.value = value;
}
range++;
}
public Minimum combine(Minimum other) {
if (value > other.value) {
index = range + other.index;
value = other.value;
}
range += other.range;
return this;
}
public int getIndex() {
return index;
}
}
static Collector<Integer, Minimum, Integer> MIN_INDEX = new Collector<Integer, Minimum, Integer>() {
@Override
public Supplier<Minimum> supplier() {
return Minimum::new;
}
@Override
public BiConsumer<Minimum, Integer> accumulator() {
return Minimum::accept;
}
@Override
public BinaryOperator<Minimum> combiner() {
return Minimum::combine;
}
@Override
public Function<Minimum, Integer> finisher() {
return Minimum::getIndex;
}
@Override
public Set<Collector.Characteristics> characteristics() {
return Collections.emptySet();
}
};
Writing a collectors creates an annoying amount of code, but it can be easily generalized to support any comparable value. Also, calling the collector looks very idiomatic:
List<Integer> list = Arrays.asList(4,3,7,1,5,2,9);
int minIndex = list.stream().collect(MIN_INDEX);
If we change the accept
and combine
methods to always return a new Minimum
instance (ie. if we make Minimum
immutable), we can also use reduce
:
int minIndex = list.stream().reduce(new Minimum(), Minimum::accept, Minimum::combine).getIndex();
I sense large potential for parallelization in this one.
Here's two possible solutions using my StreamEx library:
int idx = IntStreamEx.ofIndices(list).minBy(list::get).getAsInt();
Or:
int idx = EntryStream.of(list).minBy(Entry::getValue).get().getKey();
The second solution internally is very close to one proposed by @AlexisC. The first one is probably the fastest as it does not use boxing (internally it's a reduce operation).
Without using third-party code @Misha's answer looks the best for me.
import static java.util.Comparator.comparingInt;
int minIndex = IntStream.range(0,list.size()).boxed()
.min(comparingInt(list::get))
.get(); // or throw if empty list
As @TagirValeev mentions in his answer, you can avoid boxing by using IntStream#reduce
instead of Stream#min
, but at the cost of obscuring the intent:
int minIdx = IntStream.range(0,list.size())
.reduce((i,j) -> list.get(i) > list.get(j) ? j : i)
.getAsInt(); // or throw