Prenez chaque nième élément d'un flux Java 8


Supposons que j'ai une liste comme celle-ci:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Est-il possible d'utiliser un flux Java 8 pour prendre chaque deuxième élément de cette liste pour obtenir ce qui suit?

[1, 3, 5, 7, 9]

Ou peut-être même chaque troisième élément?

[1, 4, 7, 10]

Fondamentalement, je cherche une fonction pour prendre chaque nième élément d'un flux:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> list2 = list.stream().takenth(3).collect(Collectors.toList());
System.out.println(list2);
// => [1, 4, 7, 10]
Author: Alexis C., 2015-07-24

7 answers

L'une des principales motivations de l'introduction de Java streams était de permettre des opérations parallèles. Cela a conduit à une exigence que les opérations sur les flux Java tels que map et filter soient indépendantes de la position de l'élément dans le flux ou des éléments qui l'entourent. Cela a l'avantage de faciliter la division des flux pour le traitement parallèle. Il présente l'inconvénient de rendre certaines opérations plus complexes.

Donc la réponse simple est qu'il n'y a pas de moyen facile de faire des choses telles comme prendre chaque nième élément ou mapper chaque élément à la somme de tous les éléments précédents.

Le moyen le plus simple de mettre en œuvre votre exigence est d'utiliser l'index de la liste à partir de laquelle vous diffusez:

List<String> list = ...;
return IntStream.range(0, list.size())
    .filter(n -> n % 3 == 0)
    .mapToObj(list::get)
    .collect(Collectors.toList());

Une solution plus compliquée serait de créer un collecteur personnalisé qui collecte chaque nième élément dans une liste.

class EveryNth<C> {

    private final int nth;
    private final List<List<C>> lists = new ArrayList<>();
    private int next = 0;

    private EveryNth(int nth) {
        this.nth = nth;
        IntStream.range(0, nth).forEach(i -> lists.add(new ArrayList<>()));
    }

    private void accept(C item) {
        lists.get(next++ % nth).add(item);
    }

    private EveryNth<C> combine(EveryNth<C> other) {
        other.lists.forEach(l -> lists.get(next++ % nth).addAll(l));
        next += other.next;
        return this;
    }

    private List<C> getResult() {
        return lists.get(0);
    }

    public static Collector<Integer, ?, List<Integer>> collector(int nth) {
        return Collector.of(() -> new EveryNth(nth), 
            EveryNth::accept, EveryNth::combine, EveryNth::getResult));
}

Cela pourrait être utilisé comme suit:

List<String> list = Arrays.asList("Anne", "Bill", "Chris", "Dean", "Eve", "Fred", "George");
list.stream().parallel().collect(EveryNth.collector(3)).forEach(System.out::println);

Qui renvoie le résultat que vous attendez.

C'est un algorithme très inefficace même avec le traitement en parallèle. Il divise tous les éléments qu'il accepte en n listes, puis retourne le premier. Malheureusement, il doit garder tous les éléments à travers le processus d'accumulation, car ce n'est que lorsqu'ils sont combinés qu'il sait quelle liste est la nième. Compte tenu de sa complexité et de son inefficacité, je recommanderais certainement de s'en tenir à la solution basée sur les indices ci-dessus de préférence à cela.

 35
Author: sprinter, 2015-09-20 12:04:52

MODIFIER-Nov 28, 2017

Comme utilisateur @Emiel suggère dans les commentaires, la meilleure façon de le faire serait d'utiliser Stream.itearate pour conduire la liste à travers une séquence d'indices:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int skip = 3;
int size = list.size();
// Limit to carefully avoid IndexOutOfBoundsException
int limit = size / skip + Math.min(size % skip, 1);

List<Integer> result = Stream.iterate(0, i -> i + skip)
    .limit(limit)
    .map(list::get)
    .collect(Collectors.toList());

System.out.println(result); // [1, 4, 7, 10]

Cette approche n'a pas les inconvénients de ma réponse précédente, qui vient ci-dessous (j'ai décidé de la garder pour des raisons historiques).


Une Autre approche serait d'utiliser Stream.iterate() de la façon suivante:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int skip = 3;
int size = list.size();
// Limit to carefully avoid IndexOutOfBoundsException
int limit = size / skip + Math.min(size % skip, 1);

List<Integer> result = Stream.iterate(list, l -> l.subList(skip, l.size()))
    .limit(limit)
    .map(l -> l.get(0))
    .collect(Collectors.toList());

System.out.println(result); // [1, 4, 7, 10]

, L'idée est de créer un flux de sous-listes, chacune sauter les premiers N éléments du précédent (N=3 dans l'exemple).

Nous devons limiter le nombre d'itérations afin de ne pas essayer d'obtenir une sous-liste dont les limites sont hors de portée.

Ensuite, nous mappons nos sous-listes à leur premier élément et collectons nos résultats. Garder le premier élément de chaque sous-liste fonctionne comme prévu car l'index begin de chaque sous-liste est décalé N éléments vers la droite, selon la liste source.

Ceci est également efficace, parce que la méthode List.sublist() renvoie unview de la liste d'origine, ce qui signifie qu'elle ne crée pas de nouveau List pour chaque itération.


EDIT: Après un certain temps, j'ai appris qu'il est préférable de prendre l'une des approches de @sprinter, car subList() crée un wrapper autour de la liste d'origine. Cela signifie que la deuxième liste du volet serait un wrapper de la première liste, le troisième de la liste du volet serait un wrapper de la deuxième liste (ce qui est déjà un emballage!), et ainsi de suite...

Bien que cela puisse fonctionner pour les petites et moyennes listes, il convient de noter que pour une très grande liste source, de nombreux wrappers seraient créés. Et cela pourrait finir par coûter cher, ou même générer un StackOverflowError.

 8
Author: Federico Peralta Schaffner, 2017-11-28 14:17:55

Si vous êtes prêt à utiliser une bibliothèque tierce, alors Joooλ offre des fonctionnalités utiles comme zipWithIndex():

Chaque deuxième élément

System.out.println(
Seq.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
   .zipWithIndex()             // This produces a Tuple2(yourvalue, index)
   .filter(t -> t.v2 % 2 == 0) // Filter by the index
   .map(t -> t.v1)             // Remove the index again
   .toList()
);
[1, 3, 5, 7, 9]

Chaque troisième élément

System.out.println(
Seq.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
   .zipWithIndex()
   .filter(t -> t.v2 % 3 == 0)
   .map(t -> t.v1)
   .toList()
);
[1, 4, 7, 10]

Disclaimer: Je travaille pour la société derrière jOOλ

 7
Author: Lukas Eder, 2016-01-07 08:41:07

, Vous pouvez également utiliser flatMap, avec une fonction personnalisée qui ignore des éléments:

private <T> Function<T, Stream<T>> everyNth(int n) {
  return new Function<T, Stream<T>>() {
    int i = 0;

    @Override
    public Stream<T> apply(T t) {
      if (i++ % n == 0) {
        return Stream.of(t);
      }
      return Stream.empty();
    }
  };
}

@Test
public void everyNth() {
  assertEquals(
    Arrays.asList(1, 4, 7, 10),
    IntStream.rangeClosed(1, 10).boxed()
      .flatMap(everyNth(3))
      .collect(Collectors.toList())
  );
}

Il a l'avantage de travailler avec des flux non indexés. Mais ce n'est pas une bonne idée de l'utiliser avec des flux parallèles (peut-être passer à un entier atomique pour i).

 2
Author: Xavier, 2016-03-06 10:43:59

Essayez ceci.

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    int[] n = {0};
    List<Integer> result = list.stream()
        .filter(x -> n[0]++ % 3 == 0)
        .collect(Collectors.toList());
    System.out.println(result);
    // -> [1, 4, 7, 10]
 1
Author: saka1029, 2016-03-06 11:14:23

Voici le code par AbacusUtil

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .filter(MutableInt.of(0), (e, idx) -> idx.getAndDecrement() % 2 == 0)
        .println();
// output: 1, 3, 5, 7, 9

Ou si l'index est requis:

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
      .indexed().filter(i -> i.index() % 2 == 0).println();
// output: [0]=1, [2]=3, [4]=5, [6]=7, [8]=9

Déclaration: Je suis le développeur de AbacusUtil.

 1
Author: user_3380739, 2017-06-02 22:38:29

Utiliser la goyave:

Streams
    .mapWithIndex(stream, SimpleImmutableEntry::new)
    .filter(entry -> entry.getValue() % 3 == 0)
    .map(Entry::getKey)
    .collect(Collectors.toList());
 0
Author: ZhekaKozlov, 2018-04-05 05:10:42