web-dev-qa-db-fra.com

Recueillir des paires successives d'un flux

Avec un flux tel que { 0, 1, 2, 3, 4 },

comment puis-je le transformer le plus élégamment en une forme donnée:

{ new Pair(0, 1), new Pair(1, 2), new Pair(2, 3), new Pair(3, 4) }

(en supposant, bien sûr, que j'ai défini la classe paire)?

Edit: Cela ne concerne pas strictement les flux ints ou primitifs. La réponse devrait être générale pour un flux de n'importe quel type.

89
Aleksandr Dubinsky

Ma bibliothèque StreamEx qui étend les flux standard fournit une méthode pairMap pour tous les types de flux. Pour les flux primitifs, cela ne change pas le type de flux, mais peut être utilisé pour effectuer certains calculs. L’usage le plus courant consiste à calculer les différences:

int[] pairwiseDiffs = IntStreamEx.of(input).pairMap((a, b) -> (b-a)).toArray();

Pour le flux d'objet, vous pouvez créer tout autre type d'objet. Ma bibliothèque ne fournit aucune nouvelle structure de données visible par l'utilisateur comme Pair (c'est la partie du concept de la bibliothèque). Toutefois, si vous avez votre propre classe Pair et souhaitez l'utiliser, vous pouvez procéder comme suit:

Stream<Pair> pairs = IntStreamEx.of(input).boxed().pairMap(Pair::new);

Ou si vous avez déjà des Stream:

Stream<Pair> pairs = StreamEx.of(stream).pairMap(Pair::new);

Cette fonctionnalité est implémentée à l'aide de spliterator personnalisé . Il a des frais généraux assez faibles et peut très bien se mettre en parallèle. Bien sûr, cela fonctionne avec n'importe quelle source de flux, pas seulement avec une liste/un tableau à accès aléatoire comme dans beaucoup d'autres solutions. Dans de nombreux tests, il fonctionne vraiment bien. Voici un repère JMH où nous trouvons toutes les valeurs d'entrée précédant une valeur plus grande en utilisant différentes approches (voir this question).

30
Tagir Valeev

La bibliothèque de flux Java 8 est principalement conçue pour diviser les flux en fragments plus petits pour le traitement en parallèle, de sorte que les étapes de pipeline avec état sont assez limitées, et que vous devez par exemple obtenir l'index de l'élément de flux en cours et accéder aux éléments adjacents. les éléments de flux ne sont pas pris en charge.

Un moyen typique de résoudre ces problèmes, avec certaines limitations bien entendu, est de gérer le flux par index et de s’assurer que les valeurs sont traitées dans une structure de données à accès aléatoire telle qu’un ArrayList à partir de laquelle les éléments peuvent être extraits. Si les valeurs étaient dans arrayList, on pourrait générer les paires comme demandé en faisant quelque chose comme ceci:

    IntStream.range(1, arrayList.size())
             .mapToObj(i -> new Pair(arrayList.get(i-1), arrayList.get(i)))
             .forEach(System.out::println);

Bien entendu, la limitation est que l'entrée ne peut pas être un flux infini. Ce pipeline peut cependant être exécuté en parallèle.

66
Stuart Marks

Ce n'est pas élégant, c'est une solution bidon, mais fonctionne pour des flux infinis

Stream<Pair> pairStream = Stream.iterate(0, (i) -> i + 1).map( // natural numbers
    new Function<Integer, Pair>() {
        Integer previous;

        @Override
        public Pair apply(Integer integer) {
            Pair pair = null;
            if (previous != null) pair = new Pair(previous, integer);
            previous = integer;
            return pair;
        }
    }).skip(1); // drop first null

Maintenant, vous pouvez limiter votre flux à la longueur souhaitée

pairStream.limit(1_000_000).forEach(i -> System.out.println(i));

P.S. J'espère qu'il existe une meilleure solution, quelque chose comme clojure (partition 2 1 stream)

15
mishadoff

J'ai implémenté un wrapper de spliterator qui prend chaque n éléments T du spliterator d'origine et produit List<T>:

public class ConsecutiveSpliterator<T> implements Spliterator<List<T>> {

    private final Spliterator<T> wrappedSpliterator;

    private final int n;

    private final Deque<T> deque;

    private final Consumer<T> dequeConsumer;

    public ConsecutiveSpliterator(Spliterator<T> wrappedSpliterator, int n) {
        this.wrappedSpliterator = wrappedSpliterator;
        this.n = n;
        this.deque = new ArrayDeque<>();
        this.dequeConsumer = deque::addLast;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<T>> action) {
        deque.pollFirst();
        fillDeque();
        if (deque.size() == n) {
            List<T> list = new ArrayList<>(deque);
            action.accept(list);
            return true;
        } else {
            return false;
        }
    }

    private void fillDeque() {
        while (deque.size() < n && wrappedSpliterator.tryAdvance(dequeConsumer))
            ;
    }

    @Override
    public Spliterator<List<T>> trySplit() {
        return null;
    }

    @Override
    public long estimateSize() {
        return wrappedSpliterator.estimateSize();
    }

    @Override
    public int characteristics() {
        return wrappedSpliterator.characteristics();
    }
}

La méthode suivante peut être utilisée pour créer un flux consécutif:

public <E> Stream<List<E>> consecutiveStream(Stream<E> stream, int n) {
    Spliterator<E> spliterator = stream.spliterator();
    Spliterator<List<E>> wrapper = new ConsecutiveSpliterator<>(spliterator, n);
    return StreamSupport.stream(wrapper, false);
}

Exemple d'utilisation:

consecutiveStream(Stream.of(0, 1, 2, 3, 4, 5), 2)
    .map(list -> new Pair(list.get(0), list.get(1)))
    .forEach(System.out::println);
14
Tomek Rękawek

Vous pouvez le faire avec la méthode Stream.reduce () (je n'ai pas vu d'autres réponses utilisant cette technique).

public static <T> List<Pair<T, T>> consecutive(List<T> list) {
    List<Pair<T, T>> pairs = new LinkedList<>();
    list.stream().reduce((a, b) -> {
        pairs.add(new Pair<>(a, b));
        return b;
    });
    return pairs;
}
11
SamTebbs33

Vous pouvez le faire dans cyclops-react (je contribue à cette bibliothèque), en utilisant l'opérateur glissant.

  LazyFutureStream.of( 0, 1, 2, 3, 4 )
                  .sliding(2)
                  .map(Pair::new);

Ou

   ReactiveSeq.of( 0, 1, 2, 3, 4 )
                  .sliding(2)
                  .map(Pair::new);

En supposant que le constructeur Pair puisse accepter une collection avec 2 éléments.

Si vous vouliez grouper par 4 et incrémenter par 2, cela est également pris en charge.

     ReactiveSeq.rangeLong( 0L,Long.MAX_VALUE)
                .sliding(4,2)
                .forEach(System.out::println);

Des méthodes statiques équivalentes permettant de créer une vue glissante sur Java.util.stream.Stream sont également fournies dans la classe cyclops-streams StreamUtils .

       StreamUtils.sliding(Stream.of(1,2,3,4),2)
                  .map(Pair::new);

Remarque: - pour une opération mono-thread, ReactiveSeq serait plus approprié. LazyFutureStream étend ReactiveSeq mais est principalement destiné à une utilisation simultanée/parallèle (il s’agit d’un "Stream of Futures").

LazyFutureStream étend ReactiveSeq, qui étend Seq à partir de l’impressionnant jOOλ (qui étend Java.util.stream.Stream), de sorte que les solutions présentées par Lukas fonctionneraient également avec le type Stream. Les principales différences entre les opérateurs de fenêtre/glissement sont les différences évidentes de puissance/complexité et leur pertinence pour une utilisation avec des flux infinis (le glissement ne consomme pas le flux, mais les tampons au fur et à mesure de son flux).

6
John McClean

La bibliothèque proton-pack library fournit la fonctionnalité fenêtrée. Avec une classe Pair et un Stream, vous pouvez le faire comme ceci:

Stream<Integer> st = Stream.iterate(0 , x -> x + 1);
Stream<Pair<Integer, Integer>> pairs = StreamUtils.windowed(st, 2, 1)
                                                  .map(l -> new Pair<>(l.get(0), l.get(1)))
                                                  .moreStreamOps(...);

Maintenant, le flux pairs contient:

(0, 1)
(1, 2)
(2, 3)
(3, 4)
(4, ...) and so on
4
Alexis C.

Trouver des paires successives

Si vous souhaitez utiliser une bibliothèque tierce et que vous n'avez pas besoin de parallélisme, alors jOOλ offre les fonctions de fenêtre de style SQL suivantes:

System.out.println(
Seq.of(0, 1, 2, 3, 4)
   .window()
   .filter(w -> w.lead().isPresent())
   .map(w -> Tuple(w.value(), w.lead().get())) // alternatively, use your new Pair() class
   .toList()
);

Céder

[(0, 1), (1, 2), (2, 3), (3, 4)]

La fonction lead() accède à la valeur suivante dans l'ordre de parcours de la fenêtre.

Recherche de triples/quadruples/n-tuples successifs

Une question dans les commentaires demandait une solution plus générale, où non pas des paires mais des n-uplets (ou éventuellement des listes) devraient être collectés. Voici donc une approche alternative:

int n = 3;

System.out.println(
Seq.of(0, 1, 2, 3, 4)
   .window(0, n - 1)
   .filter(w -> w.count() == n)
   .map(w -> w.window().toList())
   .toList()
);

Produire une liste de listes

[[0, 1, 2], [1, 2, 3], [2, 3, 4]]

Sans la filter(w -> w.count() == n), le résultat serait

[[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4], [4]]

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

4
Lukas Eder

On peut utiliser RxJava (très puissant extension réactive librairie)

IntStream intStream  = IntStream.iterate(1, n -> n + 1);

Observable<List<Integer>> pairObservable = Observable.from(intStream::iterator).buffer(2,1);

pairObservable.take(10).forEach(b -> {
            b.forEach(n -> System.out.println(n));
            System.out.println();
        });

Le tampon opérateur transforme un observable qui émet des éléments en un observable qui émet des collections en tampon de ces éléments.

2
frhack

L'opération est essentiellement avec état, donc pas vraiment ce que les flux sont censés résoudre - voir la section "Comportements sans état" dans javadoc :

La meilleure approche consiste à éviter les paramètres de comportement avec état pour que les opérations de flux soient entièrement

Une solution consiste à introduire l'état dans votre flux par le biais d'un compteur externe, bien que cela ne fonctionne qu'avec un flux séquentiel.

public static void main(String[] args) {
    Stream<String> strings = Stream.of("a", "b", "c", "c");
    AtomicReference<String> previous = new AtomicReference<>();
    List<Pair> collect = strings.map(n -> {
                            String p = previous.getAndSet(n);
                            return p == null ? null : new Pair(p, n);
                        })
                        .filter(p -> p != null)
                        .collect(toList());
    System.out.println(collect);
}


static class Pair<T> {
    private T left, right;
    Pair(T left, T right) { this.left = left; this.right = right; }
    @Override public String toString() { return "{" + left + "," + right + '}'; }
}
1
assylias

Dans votre cas, je voudrais écrire ma fonction IntFunction personnalisée qui garde la trace du dernier int transmis et l’utiliser pour mapper l’IntStream d’origine.

import Java.util.function.IntFunction;
import Java.util.stream.IntStream;

public class PairFunction implements IntFunction<PairFunction.Pair> {

  public static class Pair {

    private final int first;
    private final int second;

    public Pair(int first, int second) {
      this.first = first;
      this.second = second;
    }

    @Override
    public String toString() {
      return "[" + first + "|" + second + "]";
    }
  }

  private int last;
  private boolean first = true;

  @Override
  public Pair apply(int value) {
    Pair pair = !first ? new Pair(last, value) : null;
    last = value;
    first = false;
    return pair;
  }

  public static void main(String[] args) {

    IntStream intStream = IntStream.of(0, 1, 2, 3, 4);
    final PairFunction pairFunction = new PairFunction();
    intStream.mapToObj(pairFunction)
        .filter(p -> p != null) // filter out the null
        .forEach(System.out::println); // display each Pair

  }

}
0
jpvee

J'ai enfin trouvé un moyen de tromper le Stream.reduce pour pouvoir gérer avec soin des paires de valeurs; il existe une multitude de cas d'utilisation nécessitant cette fonctionnalité qui n'apparaît pas naturellement dans JDK 8:

public static int ArithGeo(int[] arr) {
    //Geometric
    List<Integer> diffList = new ArrayList<>();
    List<Integer> divList = new ArrayList<>();
    Arrays.stream(arr).reduce((left, right) -> {
        diffList.add(right-left);
        divList.add(right/left);
        return right;
    });
    //Arithmetic
    if(diffList.stream().distinct().count() == 1) {
        return 1;
    }
    //Geometric
    if(divList.stream().distinct().count() == 1) {
        return 2;
    }
    return -1;
}

L'astuce que j'utilise est la déclaration retournez à droite;.

0
Beezer

Pour calculer les différences successives dans le temps (valeurs x) d'une série temporelle, j'utilise la méthode stream's collect(...):

final List< Long > intervals = timeSeries.data().stream()
                    .map( TimeSeries.Datum::x )
                    .collect( DifferenceCollector::new, DifferenceCollector::accept, DifferenceCollector::combine )
                    .intervals();

Où DifferenceCollector ressemble à ceci:

public class DifferenceCollector implements LongConsumer
{
    private final List< Long > intervals = new ArrayList<>();
    private Long lastTime;

    @Override
    public void accept( final long time )
    {
        if( Objects.isNull( lastTime ) )
        {
            lastTime = time;
        }
        else
        {
            intervals.add( time - lastTime );
            lastTime = time;
        }
    }

    public void combine( final DifferenceCollector other )
    {
        intervals.addAll( other.intervals );
        lastTime = other.lastTime;
    }

    public List< Long > intervals()
    {
        return intervals;
    }
}

Vous pouvez probablement modifier cela pour répondre à vos besoins.

0
Rob Philipp