5

Есть ли краткий способ итерации по стриму с индексами в Java 8?

16

Заголовок: Как итерироваться по стриму с доступом к индексу в Java?

Текст проблемы:

Я пытаюсь итерироваться по массиву строк с доступом к индексу в стриме на Java, и мне нужно сделать это в краткой форме. Вот мой текущий код:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = intRange(1, names.length).boxed();
nameList = zip(indices, stream(names), SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey())
        .map(Entry::getValue)
        .collect(toList());

Этот подход выглядит довольно неэффективным и разочаровывающим по сравнению с аналогом на LINQ:

string[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
var nameList = names.Where((c, index) => c.Length <= index + 1).ToList();

Существует ли более лаконичный способ сделать то же самое в Java? Кроме того, не могу не заметить, что метод zip, похоже, был перемещён или удалён...

5 ответ(ов)

6

Самый чистый способ — начать с потока индексов:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
IntStream.range(0, names.length)
         .filter(i -> names[i].length() <= i)
         .mapToObj(i -> names[i])
         .collect(Collectors.toList());

В результате получится список, содержащий только "Erik".


Одной из альтернатив, которая будет более привычной тем, кто использует циклы for, является использование вспомогательного счетчика с изменяемым объектом, например, AtomicInteger:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
AtomicInteger index = new AtomicInteger();
List<String> list = Arrays.stream(names)
                          .filter(n -> n.length() <= index.incrementAndGet())
                          .collect(Collectors.toList());

Обратите внимание, что использование второго метода в параллельном потоке может привести к сбоям, так как элементы не обязательно будут обрабатываться "в порядке".

1

В Java 8 API потоков отсутствуют возможности получения индекса элемента потока и объединения потоков (zip). Это, конечно, несколько неудобно, так как усложняет некоторые приложения (например, задачи, связанные с LINQ).

Тем не менее, часто существуют обходные пути. Обычно это можно сделать, "управляя" потоком с помощью целочисленного диапазона и используя тот факт, что исходные элементы часто хранятся в массиве или коллекции, доступной по индексу. Например, задачу Challenge 2 можно решить следующим образом:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList =
    IntStream.range(0, names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

Как я уже упоминал, это использует тот факт, что источник данных (массив names) можно индексировать напрямую. Если бы это не так, данный метод не сработал бы.

Признаюсь, это не совсем соответствует намерению Challenge 2. Тем не менее, это довольно эффективно решает задачу.

ИЗМЕНЕНИЕ

В моем предыдущем примере кода использовался flatMap для объединения операций фильтрации и отображения, что было неудобно и не давало никаких преимуществ. Я обновил пример согласно комментарию от Холгера.

0

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

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collector.Characteristics;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Objects.requireNonNull;

public class CollectionUtils {
    private CollectionUtils() { }

    /**
     * Преобразует {@link java.util.Iterator} в {@link java.util.stream.Stream}.
     */
    public static <T> Stream<T> iterate(Iterator<? extends T> iterator) {
        int characteristics = Spliterator.ORDERED | Spliterator.IMMUTABLE;
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, characteristics), false);
    }

    /**
     * Объединяет указанный стрим с его индексами.
     */
    public static <T> Stream<Map.Entry<Integer, T>> zipWithIndex(Stream<? extends T> stream) {
        return iterate(new Iterator<Map.Entry<Integer, T>>() {
            private final Iterator<? extends T> streamIterator = stream.iterator();
            private int index = 0;

            @Override
            public boolean hasNext() {
                return streamIterator.hasNext();
            }

            @Override
            public Map.Entry<Integer, T> next() {
                return new AbstractMap.SimpleImmutableEntry<>(index++, streamIterator.next());
            }
        });
    }

    /**
     * Возвращает стрим, содержащий результаты применения заданной функции с двумя аргументами к элементам данного стрима.
     * Первым аргументом функции является индекс элемента, а вторым - значение элемента. 
     */
    public static <T, R> Stream<R> mapWithIndex(Stream<? extends T> stream, BiFunction<Integer, ? super T, ? extends R> mapper) {
        return zipWithIndex(stream).map(entry -> mapper.apply(entry.getKey(), entry.getValue()));
    }

    public static void main(String[] args) {
        String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

        System.out.println("Тест zipWithIndex");
        zipWithIndex(Arrays.stream(names)).forEach(entry -> System.out.println(entry));

        System.out.println();
        System.out.println("Тест mapWithIndex");
        mapWithIndex(Arrays.stream(names), (Integer index, String name) -> index + "=" + name).forEach(System.out::println);
    }
}

Этот код включает несколько полезных методов. Метод zipWithIndex объединяет элементы стрима с их индексами, а mapWithIndex позволяет применять функцию к каждому элементу стрима вместе с его индексом. Это помогает избежать использования изменяемых объектов и является более чистым решением. Вы можете протестировать его, используя массив имен, как показано в методе main.

0

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

public class WithIndex<T> {
    private int index;
    private T value;

    WithIndex(int index, T value) {
        this.index = index;
        this.value = value;
    }

    public int index() {
        return index;
    }

    public T value() {
        return value;
    }

    @Override
    public String toString() {
        return value + "(" + index + ")";
    }

    public static <T> Function<T, WithIndex<T>> indexed() {
        return new Function<T, WithIndex<T>>() {
            int index = 0;
            @Override
            public WithIndex<T> apply(T t) {
                return new WithIndex<>(index++, t);
            }
        };
    }
}

Использование:

public static void main(String[] args) {
    Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
    stream.map(WithIndex.indexed()).forEachOrdered(e -> {
        System.out.println(e.index() + " -> " + e.value());
    });
}

Этот код создает поток строк, затем применяет функцию map для того, чтобы сопоставить каждому элементу его индекс, и выводит результат на консоль. Каждый элемент теперь представлен в виде пары "индекс → значение", при этом индекс автоматически увеличивается для каждого элемента в последовательности.

0

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

List<String> strings = new ArrayList<>(Arrays.asList("First", "Second", "Third", "Fourth", "Fifth")); // Пример списка строк
strings.stream() // Преобразуем список в поток
    .collect(HashMap::new, (h, o) -> h.put(h.size(), o), (h, o) -> {}) // Создаем отображение, где ключ - индекс, значение - объект
        .forEach((i, o) -> { // Теперь мы можем использовать BiConsumer для forEach!
            System.out.println(String.format("%d => %s", i, o));
        });

Результат выполнения будет следующим:

0 => First
1 => Second
2 => Third
3 => Fourth
4 => Fifth

Этот подход позволяет вам эффективно сопоставлять индексы и значения в списке с помощью потоков в Java.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь