'How to map elements to their index using streams?

I get a stream of some custom objects and I would like to create a map Map<Integer, MyObject> with index of each object as key. To give you a simple example:

Stream<String> myStream = Arrays.asList("one","two","three").stream();
Integer i = 0;
Map<Integer, String> result3 = myStream.collect(Collectors.toMap(x -> i++, x -> x));

Obviously, this doesn't compile because:

local variables referenced from a lambda expression must be final or effectively final

Is there a simple way to map elemnts of a stream to their indices so that the expected output for above example is something like:

{1=one, 2=two, 3=three}


Solution 1:[1]

You i variable is not effectively final.

You can use AtomicInteger as Integer wrapper:

Stream<String> myStream = Arrays.asList("one","two","three").stream();
AtomicInteger atomicInteger = new AtomicInteger(0);
Map<Integer, String> result3 = myStream.collect(Collectors.toMap(x -> atomicInteger.getAndIncrement(), Function.identity()));

I consider it a bit hacky because it only solves the problem of effectively final variable. Since it is a special ThreadSafe version it might introduce some overhead. Pure stream solution in the answer by Samuel Philipp might better fit your needs.

Solution 2:[2]

You can use an IntStream to solve this:

List<String> list = Arrays.asList("one","two","three");
Map<Integer, String> map = IntStream.range(0, list.size()).boxed()
        .collect(Collectors.toMap(Function.identity(), list::get));

You create an IntStream from 0 to list.size() - 1 (IntStream.range() excludes the last value from the stream) and map each index to the value in your list. The advantage of this solution is, that it will also work with parallel streams, which is not possible with the use of an AtomicInteger.

So the result in this case would be:

{0=one, 1=two, 2=three}

To start the first index at 1 you can just add 1 during collect:

List<String> list = Arrays.asList("one", "two", "three");
Map<Integer, String> map = IntStream.range(0, list.size()).boxed()
        .collect(Collectors.toMap(i -> i + 1, list::get));

This will result in this:

{1=one, 2=two, 3=three}

Solution 3:[3]

A clean solution not requiring random access source data, is

Map<Integer,String> result = Stream.of("one", "two", "three")
    .collect(HashMap::new, (m,s) -> m.put(m.size() + 1, s),
        (m1,m2) -> {
            int offset = m1.size();
            m2.forEach((i,s) -> m1.put(i + offset, s));
        });

This also works with parallel streams.

In the unlikely case that this is a recurring task, it’s worth putting the logic into a reusable collector, including some optimizations:

public static <T> Collector<T,?,Map<Integer,T>> toIndexMap() {
    return Collector.of(
        HashMap::new,
        (m,s) -> m.put(m.size() + 1, s),
        (m1,m2) -> {
            if(m1.isEmpty()) return m2;
            if(!m2.isEmpty()) {
                int offset = m1.size();
                m2.forEach((i,s) -> m1.put(i + offset, s));
            }
            return m1;
        });
}

Which can then be used like

Map<Integer,String> result = Stream.of("one", "two", "three")
    .collect(MyCollectors.toIndexMap());

or

Map<Integer,Integer> result = IntStream.rangeClosed(1, 1000)
    .boxed().parallel()
    .collect(MyCollectors.toIndexMap());

Solution 4:[4]

Try this :

Lets say String[] array = { "V","I","N","A","Y" }; then,

Arrays.stream(array) 
        .map(ele-> index.getAndIncrement() + " -> " + ele) 
        .forEach(System.out::println); 

Output :

0 -> V
1 -> I
2 -> N
3 -> A
4 -> Y

Solution 5:[5]

Guava has a static method Streams#mapWithIndex

Stream<String> myStream = Stream.of("one","two","three");
Map<Long, String> result3 = Streams.mapWithIndex(myStream, (s, i) -> Maps.immutableEntry(i + 1, s))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

// {1=one, 2=two, 3=three}
System.out.println(result3);

Solution 6:[6]

We can use the List.indexOf(Object o) method to get the index of the element in the list, while constructing the Map:

 List<String> list = Arrays.asList("one","two","three");
 Map<Integer, String> result = list.stream()
                                   .collect(Collectors.toMap(
                                    k -> list.indexOf(k) + 1, 
                                    Function.identity(),
                                    (v1, v2) -> v2));

 System.out.println(result);

If there are duplicates in the list, the index of the first occurence of the element will be added in the final map. Also to resolve the merge error in the map during a key collision we need to ensure that there is a merge function supllied to the toMap(keyMapper, valueMapper, mergeFunction)

Output:

{1=one, 2=two, 3=three}

Solution 7:[7]

Late to the party, but this problem can be solved very elegantly using a custom Collector:

List.of("one", "two", "three").stream()
            .collect(indexed())
            .forEach((number, text) -> System.out.println("%d: %s".formatted(number, text)));

The Collector which you should statically import:

public static <T, R> Collector<T, ?, Map<Integer, T>> indexed()
{
    return Collector.of(
            LinkedHashMap::new, 
            (map, element) -> map.put(map.size() +1, element), 
            (left, right) -> {left.putAll(right); return left;});
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Holger
Solution 4 Vinay Hegde
Solution 5 wilmol
Solution 6
Solution 7 David