17

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}
Michał Krzywański
  • 15,659
  • 4
  • 36
  • 63
nopens
  • 721
  • 1
  • 4
  • 20

7 Answers7

13

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}
Samuel Philipp
  • 10,631
  • 12
  • 36
  • 56
  • I had considered this as workaround, but wanted to avoid converting the stream into a list first if possible. Thank you so much for your answer anyway. – nopens Aug 24 '19 at 13:19
  • This doesn't work for streams of unknown (possibly infinite) size... – João Mendes Dec 13 '21 at 16:38
11

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.

Michał Krzywański
  • 15,659
  • 4
  • 36
  • 63
  • Didn't know anything about AtomicInteger until now. Thanks that solves my problem. – nopens Aug 24 '19 at 13:16
  • 3
    `AtomicInteger` is thread-safe and it might introduce some overhead. I consider it a bit hacky. I think that solution by SamuelPhilipp is better than this. Also notice that it only solves the problem of your variable not being effectively final. – Michał Krzywański Aug 24 '19 at 13:18
  • 5
    The irony is that this bears the costs of a thread safe operation while not being thread safe, due to relying on the processing order. [A clean solution](https://stackoverflow.com/a/57655144/2711488) without these problems, is possible… – Holger Aug 26 '19 at 09:25
  • You don't have to use AtomicInteger. A simple one-element int array would do. No more faux-thread-safety overhead. And collecting a stream to then restream it sounds like needless overhead as well. – João Mendes Dec 13 '21 at 16:37
6

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());
Holger
  • 285,553
  • 42
  • 434
  • 765
4

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);
wilmol
  • 1,429
  • 16
  • 22
2

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
Vinay Hegde
  • 1,424
  • 1
  • 10
  • 23
2

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}
Fullstack Guy
  • 16,368
  • 3
  • 29
  • 44
  • 6
    Using `indexOf` is not quite a good idea, because of two reasons: It as a worse performance for large lists as it has a time complexity of _O(n)_ (for an `ArrayList`) and if the list contains a value more than one time you get a wrong result, as `indexOf` only returns the first index, so you will overwrite the first value. – Samuel Philipp Aug 24 '19 at 13:23
  • 1
    @SamuelPhilipp what you is correct, even I have thought of that so this solution is applicable for a small list. But OP asked specifically that "Is there a simple way to map elemnts of a stream to their indices", so OP wants the actual indices from the list. And yes if there are duplicates the last occurence of the element's index will be added in the map. – Fullstack Guy Aug 24 '19 at 13:27
  • Thank you for adding one mor approach. But this produce a `java.lang.IllegalStateException: Duplicate key one` if I add a duplicate string 'one', but will work if the list contains unique elements. – nopens Aug 24 '19 at 13:29
  • 1
    @nopens yes I forgot to add the `mergeFunction` in the last parameter of `toMap` which resolves the merge error for duplicate key – Fullstack Guy Aug 24 '19 at 13:34
2

Late to the party, but this problem can be solved very elegantly using a custom Collector - which means the source of the stream can be anything beside Collection:

List<String> names = List.of("one", "two", "three");

names.stream()
.collect(indexed())
.forEach((number, name) -> System.out.println("%d: %s".formatted(number, name)));

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;});
}
David
  • 114
  • 1
  • 7