Czy istnieje zwięzły sposób na iterację strumienia z indeksami w Javie 8?


382

Czy istnieje zwięzły sposób na iterację w strumieniu, mając jednocześnie dostęp do indeksu w strumieniu?

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

co wydaje się raczej rozczarowujące w porównaniu do podanego tam przykładu LINQ

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

Czy istnieje bardziej zwięzły sposób?

Ponadto wydaje się, że zamek błyskawiczny został przesunięty lub usunięty ...


2
Co to jest intRange()? Do tej pory nie spotkałem się z tą metodą w Javie 8.
Rohit Jain

@RohitJain Prawdopodobnie an IntStream.rangeClosed(x, y).
assylias

2
Na marginesie, wyzwanie 4 wygląda lepiej (IMO) zList<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias

3
Tak, zipzostał usunięty wraz z eksperymentalnymi strumieniami o dwóch wartościach, zwanymi inaczej BiStreamlub MapStream. Głównym problemem jest to, że aby to zrobić skutecznie, Java naprawdę potrzebuje typu pary (lub krotki) o typie strukturalnym. Brakuje jednego, łatwo jest stworzyć ogólną klasę Pair lub Tuple - robiono to wiele razy - ale wszystkie usuwają się do tego samego typu.
Stuart Marks

3
Och, innym problemem z ogólną klasą Pair lub Tuple jest to, że wymaga wszystkich pudełek pierwotnych.
Stuart Marks

Odpowiedzi:


435

Najczystszym sposobem jest rozpoczęcie od strumienia indeksów:

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

Wynikowa lista zawiera tylko „Erik”.


Jedną z alternatyw, która wygląda bardziej znajomo, gdy jesteś przyzwyczajony do pętli, byłoby utrzymanie licznika ad hoc przy użyciu obiektu zmiennego, na przykład 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());

Zauważ, że użycie tej drugiej metody w równoległym strumieniu może się zepsuć, ponieważ elementy nie byłyby koniecznie przetwarzane „po kolei” .


28
Korzystanie z atomów w ten sposób jest problematyczne w przypadku równoległych strumieni. Po pierwsze, kolejność przetwarzania elementów niekoniecznie musi być taka sama jak kolejność występowania elementów w początkowej tablicy. Zatem „indeks” przypisany za pomocą atomu prawdopodobnie nie będzie pasował do rzeczywistego indeksu tablicy. Po drugie, podczas gdy atomiki są bezpieczne dla wątków, możesz napotkać spór między wieloma wątkami aktualizującymi atom, zmniejszając stopień równoległości.
Stuart Marks

1
Opracowałem rozwiązanie podobne do rozwiązania @assylias. Aby obejść problem ze wspomnianym strumieniem równoległym @StuartMark, najpierw tworzę dany strumień równoległy sekwencyjny, wykonuję mapowanie i przywracam stan równoległy. 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); } }
Daniel Dietrich,

4
@DanielDietrich Jeśli uważasz, że to rozwiązuje pytanie, powinieneś zamieścić je jako odpowiedź, a nie komentarz (kod też będzie bardziej czytelny!).
assylias

3
@DanielDietrich Przepraszamy, jeśli poprawnie czytam ten kod, to nie będzie działać. Nie możesz mieć różnych segmentów potoku działających równolegle lub sekwencyjnie. Tylko ostatni z parallellub sequentialjest honorowany, gdy rozpoczyna się operacja terminalu.
Stuart Marks

4
Ze względu na sprawiedliwość „najczystszy sposób” został skradziony z odpowiedzi @ Stuarta.
Vadzim

70

Interfejs API strumieni Java 8 nie ma funkcji pobierania indeksu elementu strumienia, a także możliwości łączenia strumieni razem. Jest to niefortunne, ponieważ sprawia, że ​​niektóre aplikacje (takie jak wyzwania LINQ) są trudniejsze niż w innym przypadku.

Jednak często występują obejścia. Zwykle można tego dokonać „sterując” strumieniem o zakresie liczb całkowitych i wykorzystując fakt, że oryginalne elementy często znajdują się w tablicy lub w kolekcji dostępnej za pomocą indeksu. Na przykład problem Challenge 2 można rozwiązać w następujący sposób:

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

Jak wspomniałem powyżej, wykorzystuje to fakt, że źródło danych (tablica nazw) można bezpośrednio indeksować. Gdyby tak nie było, ta technika nie działałaby.

Przyznaję, że to nie odpowiada celowi Wyzwania 2. Niemniej jednak rozwiązuje problem dość skutecznie.

EDYTOWAĆ

Mój poprzedni przykład kodu używał flatMapdo łączenia operacji filtrowania i mapowania, ale było to uciążliwe i nie dawało żadnych korzyści. Zaktualizowałem przykład według komentarza Holgera.


7
Jak o IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Działa bez boksu…
Holger

1
Hm, tak, dlaczego myślałem, że i tak muszę to wykorzystać flatMap?
Stuart Marks

2
W końcu powracając do tego ... Prawdopodobnie użyłem, flatMapponieważ niejako łączy operację filtrowania i mapowania w pojedynczą operację, ale tak naprawdę nie ma żadnej przewagi. Zmienię przykład.
Stuart Marks

Stream.of (Array) utworzy interfejs strumienia dla tablicy. Skutecznie zmieniając go w Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );Mniej rozpakowywania i mniej alokacji pamięci; ponieważ nie tworzymy już strumienia zasięgu.
Kod Eyez


25

W swoim projekcie wykorzystałem następujące rozwiązanie. Myślę, że jest to lepsze niż używanie zmiennych obiektów lub zakresów liczb całkowitych.

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 - był w stanie zaimplementować funkcję, która „wstawia” element, z którego korzysta każdy indeks N, StreamSupport.stream()oraz niestandardowy iterator.
ach

13

Oprócz protonpack, jOOλ za seq zapewnia tę funkcję (i bibliotek rozszerzeń, które opierają się na nim jak cyklopa reagują , jestem autorem tej biblioteki).

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

Seq obsługuje również tylko Seq.of (nazwy) i zbuduje strumień JDK pod przykryciem.

Podobnie wyglądałby odpowiednik prostej reakcji

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

Wersja z prostą reakcją jest bardziej dostosowana do przetwarzania asynchronicznego / równoległego.


John, widziałem dzisiaj twoją bibliotekę, jestem zarówno zdumiony, jak i zdezorientowany.
GOXR3PLUS,

12

Dla kompletności oto rozwiązanie obejmujące moją bibliotekę StreamEx :

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

Tutaj tworzymy EntryStream<Integer, String>rozszerzenie, które rozszerza Stream<Entry<Integer, String>>i dodaje określone operacje, takie jak filterKeyValuelub values. Używany toList()jest również skrót.


świetna robota; czy istnieje skrót .forEach(entry -> {}) ?
Steve Oh

2
@ SteveOh, jeśli dobrze rozumiem twoje pytanie, to tak, możesz pisać .forKeyValue((key, value) -> {}).
Tagir Valeev

8

Znalazłem tutaj rozwiązania, gdy strumień jest tworzony z listy lub tablicy (i znasz rozmiar). Ale co jeśli Stream ma nieznany rozmiar? W takim przypadku wypróbuj ten wariant:

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

Stosowanie:

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

Z listą możesz spróbować

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

Wynik:

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

1
Właściwie fajny pomysł, ale łańcuchy :: indexOf mogą być nieco drogie. Moją propozycją jest użycie zamiast tego: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Możesz po prostu użyć metody size (), aby utworzyć indeks.
gil.fernandes

@ gil.fernandes Dziękujemy za sugestię. Wprowadzę zmiany.
V0idst4r

3

Nie ma sposobu na iterację przez pewien czas Stream, mając dostęp do indeksu, ponieważ a Streamjest inny niż jakikolwiek Collection. A Streamjest jedynie potokiem do przenoszenia danych z jednego miejsca do drugiego, jak stwierdzono w dokumentacji :

Nie przechowywać. Strumień nie jest strukturą danych, która przechowuje elementy; zamiast tego przenoszą wartości ze źródła (którym może być struktura danych, generator, kanał IO itp.) poprzez potok operacji obliczeniowych.

Oczywiście, jak się wydaje na podpowiedź w swoim pytaniu, zawsze możesz przekonwertować swoje Stream<V>na a Collection<V>, takie jak a List<V>, w którym będziesz miał dostęp do indeksów.


2
Jest to dostępne w innych językach / narzędziach. Jest to po prostu wartość rosnąca przekazywana do funkcji mapy
Lee Campbell

Twój link do dokumentacji jest uszkodzony.
Usman Mutawakil

3

Dzięki https://github.com/poetix/protonpack możesz zrobić to 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

Jeśli nie masz nic przeciwko korzystaniu z biblioteki innej firmy, kolekcja Eclipse ma zipWithIndexi jest forEachWithIndexdostępna do użytku na wielu typach. Oto zestaw rozwiązań tego wyzwania dla zarówno typów JDK, jak i typów Eclipse Collections 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);

Oto rozwiązanie wykorzystujące forEachWithIndexzamiast tego.

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

Jeśli zmienisz lambdy na anonimowe klasy wewnętrzne powyżej, wówczas wszystkie przykłady kodu będą działały również w Javie 5-7.

Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse


2

Jeśli zdarzy ci się korzystać z Vavr (wcześniej znanego jako JavaScript), możesz skorzystać z dedykowanej metody:

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

Jeśli wydrukujemy treść, zobaczymy coś interesującego:

Stream((A, 0), ?)

Jest tak, ponieważ Streamssą leniwi i nie mamy pojęcia o kolejnych elementach w strumieniu.


1

Jeśli próbujesz uzyskać indeks oparty na predykacie, spróbuj tego:

Jeśli zależy Ci tylko na pierwszym indeksie:

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

Lub jeśli chcesz znaleźć wiele indeksów:

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

Dodaj, .orElse(-1);jeśli chcesz zwrócić wartość, jeśli jej nie znajdzie.


1

Oto kod AbacusUtil

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

Ujawnienie : Jestem programistą AbacusUtil.


1

Możesz użyć, IntStream.iterate()aby uzyskać indeks:

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

Działa to tylko dla Java 9 w górę w Javie 8, możesz użyć tego:

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

Możesz utworzyć statyczną klasę wewnętrzną, aby hermetyzować indeksator, tak jak musiałem to zrobić w przykładzie poniżej:

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

To pytanie ( Stream Way, aby uzyskać indeks pierwszego elementu pasującego do wartości logicznej ) zaznaczyło bieżące pytanie jako duplikat, więc nie mogę tam odpowiedzieć; Odpowiadam na to tutaj.

Oto ogólne rozwiązanie umożliwiające uzyskanie pasującego indeksu, który nie wymaga biblioteki zewnętrznej.

Jeśli masz listę.

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

I nazwij to tak:

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

A jeśli używasz kolekcji, wypróbuj tę.

   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

Jednym z możliwych sposobów jest indeksowanie każdego elementu w przepływie:

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

Korzystanie z anonimowej klasy w strumieniu nie jest dobrze używane, ale jest bardzo przydatne.


0

nie potrzebujesz map koniecznie
najbliższej lambda do przykładu 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);

WYDAJNOŚĆ: Sam, Pamela, Dave, Pascal, Erik

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 

Aby zebrać na liście:

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

Oczekuje się, że rozwiązaniem powyższego pytania będzie Listjeden element Erik .
Kaplan

Dodałem również przykład do zebrania na liście.
Arpan Saini

0

Jak powiedział jean-baptiste-yunès, jeśli twój strumień jest oparty na liście java, wówczas użycie AtomicInteger i jego metoda incrementAndGet jest bardzo dobrym rozwiązaniem problemu, a zwrócona liczba całkowita odpowiada indeksowi na oryginalnej liście, o ile nie używaj strumienia równoległego.


0

Jeśli potrzebujesz indeksu w forEach, zapewnia to sposób.

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

Następnie użyj go w następujący sposób.

@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()));
             });
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.