Чи є стислий спосіб перебору потоку з індексами в 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.
Rohit Jain

@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 справді потрібен структурний тип (або кортеж) типу. Не вистачаючи цього, легко створити загальний клас «Пара» або «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());

Отриманий список містить лише "Erik".


Однією з альтернатив, яка виглядає більш звичною, коли ви звикли до циклів, - це підтримка спеціального лічильника за допомогою змінного об'єкта, наприклад 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. Тим не менш, це вирішує проблему досить ефективно.

EDIT

Мій попередній приклад коду використовувався 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() );менше розпакування і менше розподілу пам'яті; оскільки ми більше не створюємо потоковий діапазон.
Code Eyez

44

Оскільки guava 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
Крім того, люди Guava ще не впроваджені дляEachWithIndex (беручи споживача, а не функцію), але це завдання: github.com/google/guava/isissue/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()та спеціальний ітератор.
ач

13

Окрім protonpack, Seq jOOλ забезпечує цю функціональність (і завдяки бібліотекам розширень, які будуються на ній, як циклоп-реагує , я є автором цієї бібліотеки).

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

Seq також підтримує лише Seq.of (імена) і створить JDK Stream під обкладинками.

Аналогічно виглядатиме еквівалент простої реакції

 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
Насправді приємна ідея, але рядки :: indexOf можуть бути трохи дорогими. Моя пропозиція використовувати замість цього: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Ви можете просто використовувати метод size () для створення індексу.
gil.fernandes

@ gil.fernandes Дякую за пропозицію. Я внесу правки.
V0idst4r

3

Немає способу ітерації Stream, не маючи доступу до індексу, тому що a Streamє на відміну від жодного Collection. A Stream- це лише трубопровід для перенесення даних з одного місця в інше, як зазначено в документації :

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

Звичайно, як вам здається, що ви натякаєте на своє запитання, ви завжди можете перетворити своє Stream<V>на таке Collection<V>, як, наприклад, a List<V>, у якому ви будете мати доступ до індексів.


2
Це доступно іншими мовами / інструментами. Це просто приростаюче значення, передане функції карти
Лі Кемпбелл

Посилання на документацію порушено.
Usman Mutawakil

3

З https://github.com/poetix/protonpack ви можете зробити це:

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 є 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 to get index of the first element boolean ) позначає поточне запитання як дублікат, тому я не можу відповісти на нього; Я відповідаю на це тут.

Ось загальне рішення для отримання відповідного індексу, який не потребує зовнішньої бібліотеки.

Якщо у вас є список.

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, то використання AtomicInteger та його методу incrementAndGet є дуже хорошим рішенням проблеми, і повернене ціле число відповідає індексу в початковому списку, якщо ви не використовуйте паралельний потік.


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.