web-dev-qa-db-fra.com

Java 8 Stream: différence entre limit () et skip ()

Parler de Streams, quand j'exécute ce morceau de code

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

Je reçois cette sortie 

A1B1C1
A2B2C2
A3B3C3

en limitant mon flux aux trois premiers composants, les actions A, B et C ne sont exécutées que trois fois.

Essayer de faire un calcul analogue sur les trois derniers éléments en utilisant la méthode skip() montre un comportement différent:

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .skip(6)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

sort cette

A1
A2
A3
A4
A5
A6
A7B7C7
A8B8C8
A9B9C9

Pourquoi, dans ce cas, les actions A1 à A6 sont-elles exécutées? Cela doit avoir quelque chose à voir avec le fait que limit est un opération intermédiaire avec état à court-circuit , alors que skip ne l'est pas, mais je ne comprends pas les implications pratiques de cette propriété. Est-ce juste que "chaque action avant skip est exécutée alors que tout le monde avant limit n'est pas"?

55
Luigi Cortese

Ce que vous avez ici sont deux pipelines de flux.

Ces pipelines de flux consistent chacun en une source, plusieurs opérations intermédiaires et une opération de terminal.

Mais les opérations intermédiaires sont paresseuses. Cela signifie que rien ne se passe sauf si une opération en aval nécessite un élément. Dans ce cas, l'opération intermédiaire fait tout ce qui est nécessaire pour produire l'élément requis, puis attend à nouveau jusqu'à ce qu'un autre élément soit demandé, etc.

Les opérations de terminal sont généralement "enthousiastes". C'est-à-dire qu'ils demandent tous les éléments du flux qui leur sont nécessaires.

Vous devez donc vraiment considérer le pipeline comme la forEach demandant au flux suivant le flux suivant, et ce flux interroge le flux situé derrière, etc., jusqu'à la source.

Dans cet esprit, voyons ce que nous avons avec votre premier pipeline:

Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));

Donc, la forEach demande le premier élément. Cela signifie que "B" peek a besoin d'un élément et demande le flux de sortie limit, ce qui signifie que limit devra demander à "A" peek, qui va à la source. Un élément est donné et va jusqu'au forEach, et vous obtenez votre première ligne:

A1B1C1

La forEach demande un autre élément, puis un autre. Et chaque fois, la demande est propagée dans le flux et exécutée. Mais lorsque forEach demande le quatrième élément, lorsque la demande parvient à limit, il sait qu'il a déjà donné tous les éléments qu'il est autorisé à donner.

Ainsi, il ne demande pas le coup d'oeil "A" pour un autre article. Il indique immédiatement que ses éléments sont épuisés et, par conséquent, plus aucune action n'est effectuée et forEach se termine.

Que se passe-t-il dans le deuxième pipeline?

    Stream.of(1,2,3,4,5,6,7,8,9)
    .peek(x->System.out.print("\nA"+x))
    .skip(6)
    .peek(x->System.out.print("B"+x))
    .forEach(x->System.out.print("C"+x));

Encore une fois, forEach demande le premier élément. Ceci est propagé en arrière. Mais quand il arrive à la skip, il sait qu'il doit demander 6 éléments en amont avant de pouvoir en passer un en aval. Donc, il fait une demande en amont de "A" peek, la consomme sans la passer en aval, fait une autre demande, etc. Donc, le coup d'oeil "A" reçoit 6 demandes pour un article et produit 6 impressions, mais ces articles ne sont pas transmis.

A1
A2
A3
A4
A5
A6

À la 7ème demande faite par skip, l'élément est transmis au peek "B" et de celui-ci à la forEach, de sorte que l'impression complète est effectuée:

A7B7C7

Alors c'est juste comme avant. skip va maintenant, chaque fois qu'il recevra une demande, demander un élément en amont et le transmettre en aval, car il "sait" qu'il a déjà effectué son travail de saut. Ainsi, le reste des impressions traverse tout le tuyau, jusqu'à épuisement de la source.

84
RealSkeptic

Il est complètement blasphématoire d’examiner individuellement les opérations de Steam car ce n’est pas ainsi qu’un flux est évalué. 

En parlant de limit (3)}, il s’agit d’une opération de court-circuit, ce qui est logique car y penser, quelle que soit l’opération avant et après la limit une limite dans un flux arrêterait l'itération après avoir obtenu n éléments till l'opération limite, mais cela ne signifie pas que seuls n éléments de flux seraient traités. Prenez cette opération de flux différente pour un exemple

public class App 
{
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .filter(x -> x%2==0)
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

serait sortie 

A1
A2B2C2
A3
A4B4C4
A5
A6B6C6

ce qui semble correct, car limit attend que 3 éléments de flux passent dans la chaîne d’opérations, bien que 6 éléments de flux soient traités.

8
Amm Sokun

Tous les flux sont basés sur des séparateurs, qui ont essentiellement deux opérations: avancer (avancer d'un élément, semblable à un itérateur) et scinder (se diviser dans une position arbitraire, ce qui convient au traitement parallèle). Vous pouvez cesser de prendre des éléments d'entrée à tout moment (ce qui est fait par limit), mais vous ne pouvez pas simplement sauter à la position arbitraire (cette opération n'existe pas dans l'interface Spliterator). Ainsi, l'opération skip doit réellement lire les premiers éléments de la source pour les ignorer. Notez que dans certains cas, vous pouvez effectuer un saut réel:

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

list.stream().skip(3)... // will read 1,2,3, but ignore them
list.subList(3, list.size()).stream()... // will actually jump over the first three elements
4
Tagir Valeev

Peut-être que ce petit diagramme aide à obtenir un "sentiment" naturel quant au traitement du flux.

La première ligne =>8=>=7=...=== décrit le flux. Les éléments 1..8 vont de gauche à droite. Il y a trois "fenêtres":

  1. Dans la première fenêtre (peek A), vous voyez tout
  2. Dans la deuxième fenêtre (skip 6 ou limit 3), un type de filtrage est effectué. Le premier ou le dernier élément est "éliminé", c'est-à-dire qu'il n'est pas transmis pour un traitement ultérieur.
  3. Dans la troisième fenêtre, vous ne voyez que les éléments qui ont été transmis

┌────────────────────────────────────────────────────────────────────────────┐ │ │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸ │ │ 8 7 6 5 4 3 2 1 │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸ │ │ │ │ │ │ │ │ skip 6 │ │ │ peek A limit 3 peek B │ └────────────────────────────────────────────────────────────────────────────┘

Probablement pas tout (peut-être même rien) dans cette explication est techniquement complètement correct. Mais quand je le vois comme ça, je sais très bien quels éléments atteignent laquelle des instructions concaténées.

0
yaccob