Parler de Stream
s, 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"?
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.
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.
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
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":
peek A
), vous voyez toutskip 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.
┌────────────────────────────────────────────────────────────────────────────┐
│ │
│▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸ │
│ 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.