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 un élément sur trois?

[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]
36
demandé sur Alexis C. 2015-07-24 07:50:51

7 réponses

L'une des principales motivations de L'introduction des flux Java était de permettre des opérations parallèles. Cela a conduit à l'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 le fractionnement des flux pour un traitement parallèle. Elle 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 parce que ce n'est pas jusqu'à ce qu'ils soient 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
répondu 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 consisterait à utiliser Stream.iterate() de la manière 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éplacé N éléments vers la droite, selon la liste source.

C'est aussi efficace, parce que la méthode List.sublist() renvoie une view 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 vaut mieux prendre l'une ou l'autre des approches de @sprinter, puisque 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 de sources, de nombreux wrappers seraient créés. Et cela pourrait finir par être coûteux, ou même générer un StackOverflowError.

8
répondu Federico Peralta Schaffner 2017-11-28 14:17:55

Si vous êtes prêt à utiliser une bibliothèque tierce, puis jOOλ 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]

Avertissement: je travaille pour la société derrière jOOλ

7
répondu 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
répondu 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
répondu 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 index 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
répondu 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
répondu ZhekaKozlov 2018-04-05 05:10:42