Как отобразить элементы на их индекс с помощью потоков?

Я получил поток некоторых пользовательских объектов, и я хотел бы создать карту Map<Integer, MyObject> с индексом каждого объекта в качестве ключа. Чтобы дать вам простой пример:

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

Очевидно, это не компилируется, потому что:

локальные переменные, на которые ссылается лямбда-выражение, должны быть окончательными или эффективно окончательный

Есть ли простой способ сопоставить элементы потока с их индексами, чтобы ожидаемый результат для приведенного выше примера был примерно таким:

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

Ответы

Ответ 1

Ваша переменная i не является окончательной.

Вы можете использовать AtomicInteger в качестве оболочки Integer:

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

Я считаю это немного хакерским, потому что это решает только проблему окончательной переменной. Поскольку это специальная версия ThreadSafe, это может привести к некоторым накладным расходам. Чистое решение stream в ответе Самуэля Филиппа может лучше соответствовать вашим потребностям.

Ответ 2

Вы можете использовать IntStream для решения этой проблемы:

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

Вы создаете IntStream из 0 в list.size() - 1 (IntStream.range() исключает последнее значение из потока) и сопоставляете каждый индекс со значением в вашем списке. Преимущество этого решения в том, что оно также будет работать с параллельными потоками, что невозможно при использовании AtomicInteger.

Таким образом, результатом в этом случае будет:

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

Чтобы начать первый индекс в 1, вы можете просто добавить 1 во время сбора:

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

Это приведет к такому:

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

Ответ 3

Чистое решение, не требующее источника данных произвольного доступа, это

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

Это также работает с параллельными потоками.

В маловероятном случае, когда это повторяющаяся задача, стоит поместить логику в сборщик многократного использования, включая некоторые оптимизации:

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

Который затем можно использовать как

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

или

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

Ответ 4

Попробуйте это:

Скажем тогда String[] array = { "V","I","N","A","Y" };,

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

Вывод:

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

Ответ 5

У гуавы есть статический метод 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);

Ответ 6

Мы можем использовать метод List.indexOf(Object o), чтобы получить индекс элемента в списке, создавая 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);

Если в списке есть дубликаты, индекс первого вхождения элемента будет добавлен в итоговую карту. Также, чтобы устранить ошибку слияния в карте во время столкновения клавиш, нам нужно убедиться, что есть функция слияния, поставляемая в toMap(keyMapper, valueMapper, mergeFunction)

Выход:

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