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


382

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

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

Есть ли более лаконичный способ?

Далее кажется, что почтовый индекс либо сдвинулся, либо был удален ...


2
Что такое intRange()? До сих пор не встречал этот метод в Java 8.
Рохит Джайн

@RohitJain вероятно IntStream.rangeClosed(x, y).
assylias

2
В качестве дополнительного комментария, задача 4 выглядит лучше (IMO) сList<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias

3
Да, zipбыл удален наряду с экспериментальными двузначными потоками, называемыми по-разному BiStreamили MapStream. Основная проблема заключается в том, что для эффективного выполнения Java действительно необходим тип структурной пары (или кортежа). Без него легко создать общий класс Pair или Tuple - это было сделано много раз - но все они стираются до одного и того же типа.
Стюарт Маркс

3
О, другая проблема с универсальным классом Pair или Tuple состоит в том, что он требует, чтобы все примитивы были упакованы.
Стюарт Маркс

Ответы:


435

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

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

Полученный список содержит только «Эрик».


Одна из альтернатив, которая выглядит более знакомой, когда вы привыкли к циклам for, - это поддерживать счетчик ad hoc, используя изменяемый объект, например 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());

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


28
Использование атомного метода таким образом проблематично с параллельными потоками. Во-первых, порядок обработки элементов не обязательно совпадает с порядком, в котором элементы встречаются в исходном массиве. Таким образом, «индекс», назначенный с использованием атомарного, вероятно, не будет соответствовать фактическому индексу массива. Во-вторых, хотя атомарность является поточно-ориентированной, вы можете столкнуться с конфликтом между несколькими потоками, обновляющими атомарную, что ухудшает степень параллелизма.
Стюарт Маркс

1
Я разработал решение, похожее на решение @assylias. Чтобы обойти проблему с упомянутым параллельным потоком @StuartMarks, я сначала делаю заданный параллельный поток последовательным, выполняю отображение и восстанавливаю параллельное состояние. public static <T> Stream<Tuple2<Integer, T>> zipWithIndex(Stream<T> stream) { final AtomicInteger index = new AtomicInteger(); final Function<T, Tuple2<Integer, T>> zipper = e -> Tuples.of(index.getAndIncrement(), e); if (stream.isParallel()) { return stream.sequential().map(zipper).parallel(); } else { return stream.map(zipper); } }
Даниэль Дитрих

4
@DanielDietrich Если вы считаете, что это решает вопрос, вы должны опубликовать его как ответ, а не как комментарий (и код также будет более читабельным!).
assylias

3
@DanielDietrich Извините, если я правильно читаю этот код, он не будет работать. Вы не можете иметь разные сегменты конвейера, работающие параллельно и последовательно. Только последний из parallelили sequentialучитывается, когда начинается работа терминала.
Стюарт Маркс

4
Справедливости ради, «самый чистый путь» был украден из ответа @ Stuart.
Вадим

70

В API потоков Java 8 отсутствуют функции получения индекса элемента потока, а также возможность объединения потоков. Это прискорбно, поскольку делает некоторые приложения (например, задачи 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(toList());

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

Я признаю, что это не удовлетворяет цели Задачи 2. Тем не менее, это действительно решает проблему достаточно эффективно.

РЕДАКТИРОВАТЬ

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


7
Как насчет IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Это работает без бокса ...
Хольгер

1
Хм, да, почему я думаю, что мне нужно было использовать в flatMapлюбом случае?
Стюарт Маркс

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

Stream.of (Array) создаст интерфейс потока для массива. Эффективно превращая это в Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );меньше распаковки и меньше выделения памяти; поскольку мы больше не создаем поток диапазона.
Код Eyez

44

Начиная с гуавы 21, вы можете использовать

Streams.mapWithIndex()

Пример (из официального документа ):

Streams.mapWithIndex(
    Stream.of("a", "b", "c"),
    (str, index) -> str + ":" + index)
) // will return Stream.of("a:0", "b:1", "c:2")

3
Кроме того, люди из Гуавы еще не реализовали forEachWithIndex (принимая пользователя, а не функцию), но это назначенная проблема: github.com/google/guava/issues/2913 .
Джон Глассмайер

25

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

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() { }

    /**
     * Converts an {@link java.util.Iterator} to {@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);
    }

    /**
     * Zips the specified stream with its indices.
     */
    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());
            }
        });
    }

    /**
     * Returns a stream consisting of the results of applying the given two-arguments function to the elements of this stream.
     * The first argument of the function is the element index and the second one - the element value. 
     */
    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("Test zipWithIndex");
        zipWithIndex(Arrays.stream(names)).forEach(entry -> System.out.println(entry));

        System.out.println();
        System.out.println("Test mapWithIndex");
        mapWithIndex(Arrays.stream(names), (Integer index, String name) -> index+"="+name).forEach((String s) -> System.out.println(s));
    }
}

+1 - смог реализовать функцию, которая «вставляет» элемент, который использует каждый N индексов, StreamSupport.stream()и пользовательский итератор.
ACH

13

В дополнение к protonpack, Seq в jOOλs предоставляет эту функциональность (и благодаря библиотекам расширений, которые основаны на нем, как cyclops-реагировать , я являюсь автором этой библиотеки).

Seq.seq(Stream.of(names)).zipWithIndex()
                         .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                         .toList();

Seq также поддерживает только Seq.of (names) и будет создавать поток JDK под прикрытиями.

Эквивалент простой реакции будет выглядеть так же, как

 LazyFutureStream.of(names)
                 .zipWithIndex()
                 .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                 .toList();

Версия с простым реагированием более приспособлена для асинхронной / параллельной обработки.


Джон, сегодня я видел твою библиотеку, я удивлен и растерян.
GOXR3PLUS

12

Просто для полноты вот решение с использованием моей библиотеки StreamEx :

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
EntryStream.of(names)
    .filterKeyValue((idx, str) -> str.length() <= idx+1)
    .values().toList();

Здесь мы создаем EntryStream<Integer, String> который расширяет Stream<Entry<Integer, String>>и добавляет некоторые конкретные операции, такие как filterKeyValueили values. Также toList()используется ярлык.


отличная работа; есть ли ярлык для .forEach(entry -> {}) ?
Стив О

2
@ SteveOh если я правильно понимаю ваш вопрос, то да, вы можете написать .forKeyValue((key, value) -> {}).
Тагир Валеев,

8

Я нашел решения здесь, когда поток создается из списка или массива (и вы знаете размер). Но что, если Stream имеет неизвестный размер? В этом случае попробуйте этот вариант:

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

6

С помощью списка вы можете попробовать

List<String> strings = new ArrayList<>(Arrays.asList("First", "Second", "Third", "Fourth", "Fifth")); // An example list of Strings
strings.stream() // Turn the list into a Stream
    .collect(HashMap::new, (h, o) -> h.put(h.size(), o), (h, o) -> {}) // Create a map of the index to the object
        .forEach((i, o) -> { // Now we can use a BiConsumer forEach!
            System.out.println(String.format("%d => %s", i, o));
        });

Вывод:

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

1
На самом деле хорошая идея, но strings :: indexOf может быть немного дороже. Я предлагаю вместо этого использовать: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Вы можете просто использовать метод size () для создания индекса.
gil.fernandes

@ gil.fernandes Спасибо за предложение. Я сделаю правки.
V0idst4r

3

Невозможно перебрать Stream, имея доступ к индексу, потому что он Streamне похож ни на один Collection. А Stream- это просто конвейер для переноса данных из одного места в другое, как указано в документации :

Нет хранения Поток - это не структура данных, в которой хранятся элементы; вместо этого они переносят значения из источника (который может быть структурой данных, генератором, каналом ввода-вывода и т. д.) через конвейер вычислительных операций.

Конечно, как вы, похоже, намекаете в своем вопросе, вы всегда можете преобразовать свое значение Stream<V>в a Collection<V>, такое как a List<V>, в котором у вас будет доступ к индексам.


2
Это доступно на других языках / инструментах. Это просто инкрементное значение, передаваемое функции карты
Ли Кэмпбелл,

Ваша ссылка на документацию не работает.
Усман Мутавакил

3

С https://github.com/poetix/protonpack вы можете сделать это zip:

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

List<String> nameList;
Stream<Integer> indices = IntStream.range(0, names.length).boxed(); 

nameList = StreamUtils.zip(indices, stream(names),SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey()).map(Entry::getValue).collect(toList());                   

System.out.println(nameList);

3

Если вы не возражаете против использования сторонней библиотеки, Eclipse Collections имеет zipWithIndexи forEachWithIndexдоступен для использования во многих типах. Вот набор решений этой проблемы для типов JDK и коллекций Eclipse zipWithIndex.

String[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
ImmutableList<String> expected = Lists.immutable.with("Erik");
Predicate<Pair<String, Integer>> predicate =
    pair -> pair.getOne().length() <= pair.getTwo() + 1;

// JDK Types
List<String> strings1 = ArrayIterate.zipWithIndex(names)
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings1);

List<String> list = Arrays.asList(names);
List<String> strings2 = ListAdapter.adapt(list)
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings2);

// Eclipse Collections types
MutableList<String> mutableNames = Lists.mutable.with(names);
MutableList<String> strings3 = mutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings3);

ImmutableList<String> immutableNames = Lists.immutable.with(names);
ImmutableList<String> strings4 = immutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings4);

MutableList<String> strings5 = mutableNames.asLazy()
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne, Lists.mutable.empty());
Assert.assertEquals(expected, strings5);

Вот решение, использующее forEachWithIndexвместо этого.

MutableList<String> mutableNames =
    Lists.mutable.with("Sam", "Pamela", "Dave", "Pascal", "Erik");
ImmutableList<String> expected = Lists.immutable.with("Erik");

List<String> actual = Lists.mutable.empty();
mutableNames.forEachWithIndex((name, index) -> {
        if (name.length() <= index + 1)
            actual.add(name);
    });
Assert.assertEquals(expected, actual);

Если вы измените лямбда-выражения на анонимные внутренние классы выше, тогда все эти примеры кода будут работать и в Java 5-7.

Примечание: я являюсь коммиттером для коллекций Eclipse


2

Если вы используете Vavr (ранее известный как Javaslang), вы можете использовать выделенный метод:

Stream.of("A", "B", "C")
  .zipWithIndex();

Если мы распечатаем контент, мы увидим что-то интересное:

Stream((A, 0), ?)

Это потому Streams, что ленивы, и мы понятия не имеем о следующих элементах в потоке.


1

Если вы пытаетесь получить индекс, основанный на предикате, попробуйте это:

Если вам важен только первый индекс:

OptionalInt index = IntStream.range(0, list.size())
    .filter(i -> list.get(i) == 3)
    .findFirst();

Или, если вы хотите найти несколько индексов:

IntStream.range(0, list.size())
   .filter(i -> list.get(i) == 3)
   .collect(Collectors.toList());

Добавьте, .orElse(-1);если вы хотите вернуть значение, если оно не найдено.


1

Вот код от AbacusUtil

Stream.of(names).indexed()
      .filter(e -> e.value().length() <= e.index())
      .map(Indexed::value).toList();

Раскрытие информации: я разработчик AbacusUtil.


1

Вы можете использовать IntStream.iterate()для получения индекса:

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

Это работает только для Java 9 и выше в Java 8, вы можете использовать это:

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

0

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

static class Indexer {
    int i = 0;
}

public static String getRegex() {
    EnumSet<MeasureUnit> range = EnumSet.allOf(MeasureUnit.class);
    StringBuilder sb = new StringBuilder();
    Indexer indexer = new Indexer();
    range.stream().forEach(
            measureUnit -> {
                sb.append(measureUnit.acronym);
                if (indexer.i < range.size() - 1)
                    sb.append("|");

                indexer.i++;
            }
    );
    return sb.toString();
}

0

Этот вопрос ( Stream Way для получения индекса первого элемента, совпадающего с логическим значением ) пометил текущий вопрос как дубликат, поэтому я не могу ответить на него там; Я отвечаю на это здесь.

Вот общее решение для получения соответствующего индекса, который не требует внешней библиотеки.

Если у вас есть список.

public static <T> int indexOf(List<T> items, Predicate<T> matches) {
        return IntStream.range(0, items.size())
                .filter(index -> matches.test(items.get(index)))
                .findFirst().orElse(-1);
}

И назовите это так:

int index = indexOf(myList, item->item.getId()==100);

И если вы используете коллекцию, попробуйте этот.

   public static <T> int indexOf(Collection<T> items, Predicate<T> matches) {
        int index = -1;
        Iterator<T> it = items.iterator();
        while (it.hasNext()) {
            index++;
            if (matches.test(it.next())) {
                return index;
            }
        }
        return -1;
    }

0

Одним из возможных способов является индексирование каждого элемента в потоке:

AtomicInteger index = new AtomicInteger();
Stream.of(names)
  .map(e->new Object() { String n=e; public i=index.getAndIncrement(); })
  .filter(o->o.n.length()<=o.i) // or do whatever you want with pairs...
  .forEach(o->System.out.println("idx:"+o.i+" nam:"+o.n));

Использование анонимного класса в потоке не очень полезно, хотя и очень полезно.


0

вам не нужно map обязательно,
что является ближайшей лямбда к примеру LINQ:

int[] idx = new int[] { 0 };
Stream.of( names ).filter( name -> name.length() <= idx[0]++ ).collect( Collectors.toList() );

0
String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
String completeString
         =  IntStream.range(0,namesArray.length)
           .mapToObj(i -> namesArray[i]) // Converting each array element into Object
           .map(String::valueOf) // Converting object to String again
           .collect(Collectors.joining(",")); // getting a Concat String of all values
        System.out.println(completeString);

ВЫХОД: Сэм, Памела, Дейв, Паскаль, Эрик

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

IntStream.range(0,namesArray.length)
               .mapToObj(i -> namesArray[i]) // Converting each array element into Object
               .map(String::valueOf) // Converting object to String again
               .forEach(s -> {
                //You can do various operation on each element here
                System.out.println(s);
               }); // getting a Concat String of all 

Собрать в списке:

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
 List<String> namesList
                =  IntStream.range(0,namesArray.length)
                .mapToObj(i -> namesArray[i]) // Converting each array element into Object
                .map(String::valueOf) // Converting object to String again
                .collect(Collectors.toList()); // collecting elements in List
        System.out.println(listWithIndex);

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

Я добавил пример для сбора в списке.
Арпан

0

Как сказал jean-baptiste-yunès, если ваш поток основан на java List, то использование AtomicInteger и его метода incrementAndGet является очень хорошим решением проблемы, и возвращаемое целое число соответствует индексу в исходном List, если вы не используйте параллельный поток.


0

Если вам нужен индекс в forEach, то это обеспечивает способ.

  public class IndexedValue {

    private final int    index;
    private final Object value;

    public IndexedValue(final int index, final Object value) { 
        this.index = index;
        this.value = value;
    }

    public int getIndex() {
        return index;
    }

    public Object getValue() {
        return value;
    }
}

Затем используйте его следующим образом.

@Test
public void withIndex() {
    final List<String> list = Arrays.asList("a", "b");
    IntStream.range(0, list.size())
             .mapToObj(index -> new IndexedValue(index, list.get(index)))
             .forEach(indexValue -> {
                 System.out.println(String.format("%d, %s",
                                                  indexValue.getIndex(),
                                                  indexValue.getValue().toString()));
             });
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.