Existe-t-il un moyen concis d'itérer sur un flux avec des indices en Java 8?


382

Existe-t-il un moyen concis d'itérer sur un flux tout en ayant accès à l'index dans le flux?

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

ce qui semble plutôt décevant par rapport à l'exemple LINQ qui y est donné

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

Existe-t-il un moyen plus concis?

De plus, il semble que le zip a été déplacé ou supprimé ...


2
Qu'est-ce que c'est intRange()? Je n'ai pas rencontré cette méthode dans Java 8 jusqu'à présent.
Rohit Jain

@RohitJain Probablement un IntStream.rangeClosed(x, y).
assylias

2
En guise de commentaire secondaire, le défi 4 semble meilleur (OMI) avecList<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias

3
Oui, a zipété supprimé, ainsi que les flux expérimentaux à deux valeurs, appelés BiStreamou ou MapStream. Le problème principal est que pour le faire efficacement, Java a vraiment besoin d'un type de paire (ou tuple) typé structurellement. À défaut, il est facile de créer une classe Pair ou Tuple générique - cela a été fait plusieurs fois - mais ils effacent tous le même type.
Stuart marque le

3
Oh, un autre problème avec une classe Pair ou Tuple générique est qu'elle nécessite que toutes les primitives soient encadrées.
Stuart marque le

Réponses:


435

La façon la plus propre est de partir d'un flux d'indices:

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

La liste résultante contient uniquement "Erik".


Une alternative qui semble plus familière lorsque vous êtes habitué aux boucles serait de maintenir un compteur ad hoc en utilisant un objet modifiable, par exemple 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());

Notez que l' utilisation de cette dernière méthode sur un flux parallèle pourrait se casser car les éléments ne seraient pas nécessairement traités "dans l'ordre" .


28
L'utilisation atomique de cette façon est problématique avec les flux parallèles. Premièrement, l'ordre de traitement des éléments ne sera pas nécessairement le même que l'ordre dans lequel les éléments se produisent dans le tableau initial. Ainsi, l '"index" attribué à l'aide de l'atomique ne correspondra probablement pas à l'index du tableau réel. Deuxièmement, alors que les atomiques sont thread-safe, vous pouvez rencontrer des conflits entre plusieurs threads mettant à jour l'atomique, dégradant la quantité de parallélisme.
Stuart marque le

1
J'ai développé une solution similaire à celle de @assylias. Pour contourner la problématique du flux parallèle @StuartMarks mentionné, je fais d'abord un flux parallèle donné séquentiel, effectue le mappage et restaure l'état parallèle. 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 Si vous pensez que cela résout la question, vous devez le poster comme réponse plutôt que comme commentaire (et le code sera également plus lisible!).
assylias

3
@DanielDietrich Désolé, si je lis ce code correctement, cela ne fonctionnera pas. Vous ne pouvez pas avoir différents segments d'un pipeline en cours d'exécution en parallèle ou séquentiel. Seul le dernier parallelou sequentialest honoré lorsque l'opération de terminal commence.
Stuart marque

4
Par souci de justice, "la voie la plus propre" a été volée à la réponse de @ Stuart.
Vadzim

70

L'API de flux Java 8 n'a pas les fonctionnalités permettant d'obtenir l'index d'un élément de flux ainsi que la possibilité de compresser les flux ensemble. C'est regrettable, car cela rend certaines applications (comme les défis LINQ) plus difficiles qu'elles ne le seraient autrement.

Il existe cependant souvent des solutions de contournement. Habituellement, cela peut être fait en "pilotant" le flux avec une plage entière, et en profitant du fait que les éléments d'origine sont souvent dans un tableau ou dans une collection accessible par index. Par exemple, le problème du Challenge 2 peut être résolu de cette façon:

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

Comme je l'ai mentionné ci-dessus, cela profite du fait que la source de données (le tableau des noms) est directement indexable. Si ce n'était pas le cas, cette technique ne fonctionnerait pas.

J'admets que cela ne répond pas à l'intention du défi 2. Néanmoins, cela résout le problème de manière raisonnablement efficace.

ÉDITER

Mon exemple de code précédent avait l'habitude flatMapde fusionner les opérations de filtrage et de mappage, mais c'était lourd et n'offrait aucun avantage. J'ai mis à jour l'exemple par le commentaire de Holger.


7
Et alors IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Ça marche sans boxe…
Holger

1
Hm, ouais, pourquoi ai-je pensé que je devais utiliser de flatMaptoute façon?
Stuart marque

2
Enfin, revisitant ceci ... J'ai probablement utilisé flatMapparce qu'il fusionne en quelque sorte une opération de filtrage et de mappage en une seule opération, mais cela ne fournit vraiment aucun avantage. Je vais éditer l'exemple.
Stuart marque le

Stream.of (Array) créera une interface de flux pour un tableau. Transformer efficacement en Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );moins unboxing et moins d'allocation de mémoire; car nous ne créons plus de flux de plage.
Code Eyez

44

Depuis la goyave 21, vous pouvez utiliser

Streams.mapWithIndex()

Exemple (extrait du doc officiel ):

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

3
De plus, les gens de Guava n'ont pas encore implémenté forEachWithIndex (en prenant un consommateur plutôt qu'une fonction), mais c'est un problème attribué: github.com/google/guava/issues/2913 .
John Glassmyer

25

J'ai utilisé la solution suivante dans mon projet. Je pense que c'est mieux que d'utiliser des objets mutables ou des plages entières.

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 - a pu implémenter une fonction qui "insère" un élément tous les N index à l'aide StreamSupport.stream()et un itérateur personnalisé.
ach

13

En plus de protonpack, la Seq de jOOλ fournit cette fonctionnalité (et par extension des bibliothèques qui s'appuient dessus comme cyclops-react , je suis l'auteur de cette bibliothèque).

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

Seq prend également en charge uniquement Seq.of (noms) et créera un flux JDK sous les couvertures.

L'équivalent à réaction simple ressemblerait à

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

La version simple-react est plus adaptée au traitement asynchrone / simultané.


John, j'ai vu aujourd'hui votre bibliothèque, je suis à la fois étonné et confus.
GOXR3PLUS

12

Juste pour être complet, voici la solution impliquant ma bibliothèque StreamEx :

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

Ici, nous créons un EntryStream<Integer, String>qui étend Stream<Entry<Integer, String>>et ajoute des opérations spécifiques comme filterKeyValueou values. Un toList()raccourci est également utilisé.


bon travail; existe-t-il un raccourci pour .forEach(entry -> {}) ?
Steve Oh

2
@SteveOh si je comprends bien votre question, alors oui, vous pouvez écrire .forKeyValue((key, value) -> {}).
Tagir Valeev

8

J'ai trouvé les solutions ici lorsque le Stream est créé de liste ou de tableau (et vous connaissez la taille). Mais que se passe-t-il si Stream est de taille inconnue? Dans ce cas, essayez cette 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);
            }
        };
    }
}

Usage:

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

Avec une liste, vous pouvez essayer

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

Production:

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

1
En fait, c'est une bonne idée, mais strings :: indexOf peut être un peu cher. Ma suggestion est d'utiliser à la place: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Vous pouvez simplement utiliser la méthode size () pour créer l'index.
gil.fernandes

@ gil.fernandes Merci pour la suggestion. Je ferai les modifications.
V0idst4r

3

Il n'y a aucun moyen d'itérer sur a Streamtout en ayant accès à l'index car a Streamne ressemble à aucun Collection. A Streamest simplement un pipeline pour transporter des données d'un endroit à un autre, comme indiqué dans la documentation :

Pas de stockage. Un flux n'est pas une structure de données qui stocke des éléments; au lieu de cela, ils transportent les valeurs d'une source (qui pourrait être une structure de données, un générateur, un canal d'E / S, etc.) via un pipeline d'opérations de calcul.

Bien sûr, comme vous semblez le laisser entendre dans votre question, vous pouvez toujours convertir votre fichier Stream<V>en un Collection<V>, comme un List<V>, dans lequel vous aurez accès aux index.


2
Ceci est disponible dans d'autres langues / outils. Il s'agit simplement d'une valeur incrémentielle transmise à la fonction de carte
Lee Campbell

Votre lien vers la documentation est rompu.
Usman Mutawakil

3

Avec https://github.com/poetix/protonpack, vous pouvez faire ce 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

Si cela ne vous dérange pas d'utiliser une bibliothèque tierce, Eclipse Collections a zipWithIndexet est forEachWithIndexdisponible pour une utilisation sur de nombreux types. Voici un ensemble de solutions à ce défi pour les types JDK et les types de collections Eclipse utilisant 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);

Voici une solution utilisant à la forEachWithIndexplace.

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

Si vous modifiez les lambdas en classes internes anonymes ci-dessus, tous ces exemples de code fonctionneront également en Java 5-7.

Remarque: je suis un committer pour les collections Eclipse


2

Si vous utilisez Vavr (anciennement Javaslang), vous pouvez tirer parti de la méthode dédiée:

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

Si nous imprimons le contenu, nous verrons quelque chose d'intéressant:

Stream((A, 0), ?)

En effet, nous Streamssommes paresseux et nous n'avons aucune idée des éléments suivants dans le flux.


1

Si vous essayez d'obtenir un index basé sur un prédicat, essayez ceci:

Si vous ne vous souciez que du premier index:

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

Ou si vous souhaitez rechercher plusieurs index:

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

Ajoutez .orElse(-1);au cas où vous voudriez retourner une valeur s'il ne la trouve pas.


1

Voici le code d' AbacusUtil

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

Divulgation : Je suis le développeur d'AbacusUtil.


1

Vous pouvez utiliser IntStream.iterate()pour obtenir l'index:

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

Cela ne fonctionne que pour Java 9 vers le haut dans Java 8, vous pouvez utiliser ceci:

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

Vous pouvez créer une classe interne statique pour encapsuler l'indexeur comme je devais le faire dans l'exemple ci-dessous:

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

Cette question ( Stream Way pour obtenir l'indice du premier élément correspondant à un booléen ) a marqué la question actuelle comme un doublon, donc je ne peux pas y répondre; Je réponds ici.

Voici une solution générique pour obtenir l'index correspondant qui ne nécessite pas de bibliothèque externe.

Si vous avez une liste.

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

Et appelez-le comme ceci:

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

Et si vous utilisez une collection, essayez celle-ci.

   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

Une façon possible consiste à indexer chaque élément sur le flux:

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'utilisation d'une classe anonyme le long d'un flux n'est pas bien utilisée tout en étant très utile.


0

vous n'avez pas besoin map nécessairement
qui est le plus proche lambda à l'exemple de 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);

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

Pour collecter dans la liste:

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 solution de la question ci-dessus devrait être un ErikList contenant un élément .
Kaplan

J'ai également ajouté un exemple à collecter dans la liste.
Arpan Saini

0

Comme l'a dit Jean-Baptiste-Yunès, si votre flux est basé sur une liste Java, l'utilisation d'un AtomicInteger et de sa méthode incrementAndGet est une très bonne solution au problème et l'entier renvoyé correspond à l'index de la liste d'origine tant que vous n'utilisez pas de flux parallèle.


0

Si vous avez besoin de l'index dans forEach, cela fournit un moyen.

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

Ensuite, utilisez-le comme suit.

@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()));
             });
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.