web-dev-qa-db-fra.com

Prendre 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 recherche 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]
41
Michel Krämer

L'une des principales motivations pour l'introduction de Java streams était de permettre des opérations parallèles. Cela a conduit à l'exigence que les opérations sur Java streams telles que map et filter sont indépendants 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 un traitement parallèle. Il a l'inconvénient de rendre certaines opérations plus complexe.

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

La façon la 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.

Il s'agit d'un algorithme très inefficace même avec un traitement parallèle. Il divise tous les éléments qu'il accepte en n listes, puis renvoie simplement le premier. Malheureusement, il doit conserver tous les éléments tout au long du 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.

36
sprinter

EDIT - 28 nov.2017

Comme l'utilisateur @Emiel le 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'index:

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 conserver 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 sautant 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 limites.

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

Ceci est également efficace, car la méthode List.sublist() renvoie une vue de la liste d'origine, ce qui signifie qu'elle ne crée pas de nouvelle 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 flux serait un wrapper de la première liste, la troisième liste du flux serait un wrapper de la deuxième liste (qui est déjà un wrapper!), 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 seront créés. Et cela pourrait finir par être coûteux, voire générer un StackOverflowError.

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

Un élément sur deux

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 l'entreprise derrière jOOλ

7
Lukas Eder

Vous pouvez également utiliser flatMap avec une fonction personnalisée qui ignore les é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 basculer vers un entier atomique pour i).

2
Xavier

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 d'AbacusUtil.

1
user_3380739

Utilisez la goyave:

Streams
    .mapWithIndex(stream, SimpleImmutableEntry::new)
    .filter(entry -> entry.getValue() % 3 == 0)
    .map(Entry::getKey)
    .collect(Collectors.toList());
1
ZhekaKozlov

Essaye ça.

    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
saka1029

Je viens ici de Comment éviter un débordement de mémoire en utilisant un débit élevé Java Flux d'E/S à partir de connecteurs JDBC? ce qui suggère que vous vous inquiétez de l'empreinte.

Je suggère donc la solution suivante qui devrait avoir un petit taux de collecte des ordures

int[] counter = new int[]{0};

list.stream()
.filter(l -> counter[0]++ % n == 0)

Bien sûr, vous devez vous assurer que votre flux n'est pas parallèle.

0
Jens Schauder

Pouvez-vous essayer ceci

employees.stream()
.filter(e -> e.getName().charAt(0) == 's')
.skip(n-1)
.findFirst()
0
mimi