Esiste un modo conciso per scorrere su uno stream con indici in Java 8?


382

Esiste un modo conciso per scorrere su uno stream pur avendo accesso all'indice nello stream?

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

che sembra piuttosto deludente rispetto all'esempio LINQ qui fornito

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

C'è un modo più conciso?

Inoltre sembra che la zip sia stata spostata o rimossa ...


2
Che cosa è intRange()? Fino ad ora non ho mai incontrato questo metodo in Java 8.
Rohit Jain,

@RohitJain Probabilmente un IntStream.rangeClosed(x, y).
Assylias,

2
Come commento secondario, la sfida 4 sembra migliore (IMO) conList<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias,

3
Sì, è zipstato rimosso, insieme a flussi sperimentali a due valori variamente chiamati BiStreamo MapStream. Il problema principale è che per farlo efficacemente Java ha davvero bisogno di un tipo di coppia strutturata (o tupla). In mancanza di uno, è facile creare una classe Pair o Tuple generica - è stata fatta molte volte - ma cancellano tutti dello stesso tipo.
Stuart Marks

3
Oh, un altro problema con una classe Pair o Tuple generica è che richiede l'inscatolamento di tutte le primitive.
Stuart Marks

Risposte:


435

Il modo più pulito è iniziare da un flusso di indici:

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

L'elenco risultante contiene solo "Erik".


Un'alternativa che sembra più familiare quando si è abituati ai loop sarebbe quella di mantenere un contatore ad hoc usando un oggetto mutabile, ad esempio un 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());

Si noti che l' utilizzo di quest'ultimo metodo su un flusso parallelo potrebbe interrompersi poiché gli articoli non sarebbero necessariamente elaborati "in ordine" .


28
L'uso dell'atomica in questo modo è problematico con flussi paralleli. Innanzitutto, l'ordine di elaborazione degli elementi non sarà necessariamente lo stesso dell'ordine in cui si verificano gli elementi nella matrice iniziale. Pertanto, l '"indice" assegnato utilizzando l'atomico probabilmente non corrisponderà all'indice di array effettivo. In secondo luogo, mentre gli atomici sono sicuri per i thread, è possibile riscontrare contese tra più thread che aggiornano l'atomico, degradando la quantità di parallelismo.
Stuart Marks

1
Ho sviluppato una soluzione simile a quella di @assylias. Per aggirare la problematica con il flusso parallelo @StuartMarks menzionato, per prima cosa faccio un dato flusso parallelo sequenziale, eseguo il mapping e ripristino lo stato parallelo. 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 Se pensi che risolva la domanda, dovresti pubblicarla come risposta anziché come commento (e anche il codice sarà più leggibile!).
Assylias,

3
@DanielDietrich Spiacente, se sto leggendo correttamente quel codice, non funzionerà. Non è possibile avere diversi segmenti di una pipeline in esecuzione in parallelo o in sequenza. Solo l'ultimo parallelo sequentialè onorato quando inizia l'operazione del terminale.
Stuart segna il

4
Per motivi di giustizia, "la via più pulita" è stata rubata dalla risposta di @ Stuart.
Vadzim,

70

L'API degli stream Java 8 non ha le caratteristiche per ottenere l'indice di un elemento stream e la possibilità di comprimere gli stream insieme. Questo è un peccato, poiché rende alcune applicazioni (come le sfide LINQ) più difficili di quanto non sarebbero altrimenti.

Tuttavia, ci sono spesso soluzioni alternative. Di solito questo può essere fatto "guidando" il flusso con un intervallo intero e sfruttando il fatto che gli elementi originali sono spesso in una matrice o in una raccolta accessibile per indice. Ad esempio, il problema Challenge 2 può essere risolto in questo modo:

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

Come accennato in precedenza, ciò sfrutta il fatto che l'origine dati (l'array dei nomi) è direttamente indicizzabile. Se non lo fosse, questa tecnica non funzionerebbe.

Devo ammettere che questo non soddisfa l'intento della Sfida 2. Ciò nonostante risolve il problema in modo ragionevolmente efficace.

MODIFICARE

Il mio precedente esempio di codice usato flatMapper fondere le operazioni di filtro e mappa, ma questo era ingombrante e non forniva alcun vantaggio. Ho aggiornato l'esempio per il commento di Holger.


7
Che ne dici IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Funziona senza boxe ...
Holger

1
Hm, sì, perché pensavo di dover usare flatMapcomunque?
Stuart segna il

2
Infine rivisitando questo ... probabilmente l'ho usato flatMapperché in un certo senso fonde un'operazione di filtraggio e mappatura in una singola operazione, ma questo non offre davvero alcun vantaggio. Modificherò l'esempio.
Stuart segna il

Stream.of (Array) creerà un'interfaccia di flusso per un array. Rendendolo efficacemente in Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );meno unboxing e meno allocazione di memoria; poiché non stiamo più creando un flusso di intervallo.
Codice Eyez,

44

Da guava 21 puoi usare

Streams.mapWithIndex()

Esempio (dal documento ufficiale ):

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

3
Inoltre, la gente di Guava non ha ancora implementato per ogniachithithdede (prendendo un consumatore piuttosto che una funzione), ma è un problema assegnato: github.com/google/guava/issues/2913 .
John Glassmyer,

25

Ho usato la seguente soluzione nel mio progetto. Penso che sia meglio che usare oggetti mutabili o intervalli di numeri interi.

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: è stato in grado di implementare una funzione che "inserisce" un elemento utilizzato da ogni N indici StreamSupport.stream()e un iteratore personalizzato.
ach

13

Oltre a protonpack, Seq di jOOλ offre questa funzionalità (e tramite le librerie di estensione che si basano su di esso come cyclops-reazioni , sono l'autore di questa libreria).

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

Seq supporta anche solo Seq.of (nomi) e costruirà un flusso JDK sotto le copertine.

L'equivalente a reazione semplice sarebbe simile

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

La versione a reazione semplice è più personalizzata per l'elaborazione asincrona / simultanea.


Giovanni ho visto oggi la tua biblioteca, sono sia stupito che confuso.
GOXR3PLUS

12

Solo per completezza ecco la soluzione che coinvolge la mia libreria StreamEx :

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

Qui creiamo un oggetto EntryStream<Integer, String>che estende Stream<Entry<Integer, String>>e aggiunge alcune operazioni specifiche come filterKeyValueo values. Viene toList()utilizzato anche il collegamento.


ottimo lavoro; c'è una scorciatoia per .forEach(entry -> {}) ?
Steve Oh,

2
@SteveOh se ho capito bene la tua domanda, allora sì, puoi scrivere .forKeyValue((key, value) -> {}).
Tagir Valeev,

8

Ho trovato le soluzioni qui quando viene creato lo Stream di elenco o array (e conosci le dimensioni). E se Stream avesse dimensioni sconosciute? In questo caso prova questa variante:

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

Uso:

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

Con un elenco puoi provare

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

Produzione:

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

1
In realtà una buona idea, ma stringhe :: indexOf potrebbe essere un po 'costoso. Il mio suggerimento è di usare invece: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Puoi semplicemente usare il metodo size () per creare l'indice.
gil.fernandes,

@ gil.fernandes Grazie per il suggerimento. Farò le modifiche.
V0idst4r,

3

Non c'è modo di iterare su un po ' Streammentre si ha accesso all'indice perché a Streamè diverso da qualsiasi Collection. A Streamè semplicemente una pipeline per il trasporto di dati da un luogo a un altro, come indicato nella documentazione :

Nessuna memoria. Un flusso non è una struttura di dati che archivia elementi; invece, trasportano valori da una sorgente (che potrebbe essere una struttura di dati, un generatore, un canale IO, ecc.) attraverso una pipeline di operazioni computazionali.

Ovviamente, come sembra suggerire una domanda, puoi sempre convertire la tua Stream<V>in a Collection<V>, come a List<V>, in cui avrai accesso agli indici.


2
Questo è disponibile in altre lingue / strumenti. È semplicemente un valore incrementale passato alla funzione mappa
Lee Campbell,

Il tuo link alla documentazione è interrotto.
Usman Mutawakil,

3

Con https://github.com/poetix/protonpack puoi farlo 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

Se non ti dispiace utilizzando una libreria di terze parti, Eclipse Collezioni ha zipWithIndexe forEachWithIndexdisponibile per l'uso in molti tipi. Ecco una serie di soluzioni a questa sfida sia per i tipi JDK che per i tipi 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);

Ecco una soluzione che utilizza forEachWithIndexinvece.

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

Se si modificano le lambda in classi interne anonime sopra, tutti questi esempi di codice funzioneranno anche in Java 5 - 7.

Nota: sono un committer per le raccolte Eclipse


2

Se ti capita di usare Vavr (precedentemente noto come Javaslang), puoi sfruttare il metodo dedicato:

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

Se stampiamo il contenuto, vedremo qualcosa di interessante:

Stream((A, 0), ?)

Questo perché Streamssono pigri e non abbiamo idea dei prossimi elementi nello stream.


1

Se stai cercando di ottenere un indice basato su un predicato, prova questo:

Se ti interessa solo il primo indice:

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

O se vuoi trovare più indici:

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

Aggiungi .orElse(-1);nel caso in cui desideri restituire un valore se non lo trova.


1

Ecco il codice di AbacusUtil

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

Divulgazione : Sono lo sviluppatore di AbacusUtil.


1

Puoi usare IntStream.iterate()per ottenere l'indice:

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

Questo funziona solo per Java 9 e versioni successive in Java 8 è possibile utilizzare questo:

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

È possibile creare una classe interna statica per incapsulare l'indicizzatore come avevo bisogno di fare nell'esempio seguente:

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

Questa domanda ( Stream Way per ottenere l'indice del primo elemento corrispondente booleano ) ha contrassegnato la domanda corrente come duplicata, quindi non posso rispondere lì; Sto rispondendo qui.

Ecco una soluzione generica per ottenere l'indice corrispondente che non richiede una libreria esterna.

Se hai un elenco.

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

E chiamalo così:

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

E se usi una collezione, prova questa.

   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

Un modo possibile è indicizzare ciascun elemento sul flusso:

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

L'uso di una classe anonima lungo uno stream non è ben utilizzato, sebbene sia molto utile.


0

non è necessario map necessariamente un
lambda più vicino all'esempio 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);

OUTPUT: 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 

Per raccogliere nell'elenco:

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

La soluzione della domanda precedente dovrebbe essere un Listelemento Erik contenente un elemento .
Kaplan,

Ho aggiunto un esempio da raccogliere anche nell'elenco.
Arpan Saini,

0

Come ha detto jean-baptiste-yunès, se il tuo stream è basato su un elenco java, utilizzare un AtomicInteger e il suo metodo incrementAndGet è un'ottima soluzione al problema e il numero intero restituito corrisponde all'indice nell'elenco originale purché tu non utilizzare un flusso parallelo.


0

Se hai bisogno dell'indice in forEach, questo fornisce un modo.

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

Quindi usalo come segue.

@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()));
             });
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.