it-swarm.com.de

Java 8 Stream: Unterschied zwischen limit () und überspringen ()

Apropos Streams, wenn ich diesen Code ausführe

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));
    }
}

Ich bekomme diese Ausgabe 

A1B1C1
A2B2C2
A3B3C3

das Beschränken meines Streams auf die ersten drei Komponenten zwingt die Aktionen A, B und C nur dreimal auszuführen.

Der Versuch, eine analoge Berechnung der letzten drei Elemente mit der skip()-Methode durchzuführen, zeigt ein anderes Verhalten: dies

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));
    }
}

gibt dies aus

A1
A2
A3
A4
A5
A6
A7B7C7
A8B8C8
A9B9C9

Warum werden in diesem Fall die Aktionen A1 bis A6 ausgeführt? Es muss etwas damit zu tun haben, dass limit eine stateful-Zwischenschaltung mit Kurzschluss ist, während überspringen dies nicht ist, aber ich verstehe die praktischen Auswirkungen dieser Eigenschaft nicht . Ist es nur so, dass "jede Aktion vor überspringen ausgeführt wird, während nicht alle vor limit sind"?

55
Luigi Cortese

Was Sie hier haben, sind zwei Stream-Pipelines.

Diese Stream-Pipelines bestehen jeweils aus einer Quelle, mehreren Zwischenoperationen und einer Terminaloperation.

Aber die Zwischenoperationen sind faul. Dies bedeutet, dass nichts passiert, es sei denn, ein nachgeschalteter Vorgang erfordert einen Artikel. Wenn dies der Fall ist, erledigt die Zwischenoperation alles, um den erforderlichen Artikel herzustellen, und wartet dann erneut, bis ein anderer Artikel angefordert wird und so weiter.

Der Terminalbetrieb ist normalerweise "eifrig". Das heißt, sie fragen nach allen Elementen im Stream, die für den Abschluss erforderlich sind.

Sie sollten sich also wirklich die Pipeline als forEach vorstellen, die den Stream dahinter nach dem nächsten Element fragt, und dieser Stream fragt den Stream dahinter und so weiter bis zur Quelle.

In diesem Sinne wollen wir sehen, was wir mit Ihrer ersten Pipeline haben:

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));

Das forEach fragt also nach dem ersten Artikel. Das heißt, das "B" peek benötigt ein Element und fragt den limit-Ausgabestrom danach, was bedeutet, dass limit das "A" peek fragen muss, das zur Quelle geht. Ein Gegenstand ist gegeben und geht den ganzen Weg bis zum forEach, und Sie erhalten Ihre erste Zeile:

A1B1C1

Das forEach fragt nach einem anderen Element und dann nach einem anderen. Und jedes Mal wird die Anforderung im Stream weitergeleitet und ausgeführt. Wenn forEach nach dem vierten Element fragt, wenn die Anforderung an limit gelangt, weiß er, dass er bereits alle Elemente angegeben hat, die er geben darf.

Daher wird der "A" - Blick nicht nach einem anderen Element gefragt. Es zeigt sofort an, dass seine Elemente erschöpft sind und daher keine weiteren Aktionen ausgeführt werden und forEach beendet wird.

Was passiert in der zweiten 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));

forEach fragt erneut nach dem ersten Artikel. Dies wird zurückgesandt. Wenn es jedoch an die skip gelangt, weiß es, dass es 6 Upstream-Elemente von seinem Upstream anfordern muss, bevor es einen Downstream passieren kann. Es macht also eine Anfrage stromaufwärts von "A" peek, verbraucht sie, ohne sie stromabwärts zu übergeben, stellt eine andere Anfrage und so weiter. Der A-Blick erhält also 6 Anforderungen für einen Artikel und erzeugt 6 Drucke, die jedoch nicht weitergegeben werden.

A1
A2
A3
A4
A5
A6

Bei der siebten Anforderung von skip wird der Artikel an den "B" -Pick weitergeleitet und von dort an den forEach, sodass der vollständige Druck ausgeführt wird:

A7B7C7

Dann ist es genauso wie vorher. Das skip fragt jetzt, wann immer es eine Anforderung erhält, nach einem Artikel, der stromaufwärts liegt, und leitet es stromabwärts weiter, da es "weiß", dass es seinen Überspringauftrag bereits ausgeführt hat. Der Rest der Drucke durchläuft also die gesamte Leitung, bis die Quelle erschöpft ist.

84
RealSkeptic

Die fließende Notation der gestreamten Pipeline ist der Grund für diese Verwirrung. Denken Sie so darüber nach:

limit(3)

Alle Pipeline-Operationen werden träge ausgewertet, mit Ausnahme von forEach(), bei dem es sich um eine Terminal-Operation handelt, die "Ausführung der Pipeline" auslöst.

Wenn die Pipeline ausgeführt wird, gehen die Intermediary Stream-Definitionen nicht davon aus, was passiert "vor" oder "nach". Sie nehmen lediglich einen Eingabestream und wandeln ihn in einen Ausgabestream um:

_Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.limit(3);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
_
  • _s1_ enthält 9 verschiedene Integer Werte.
  • _s2_ zeigt alle übergebenen Werte an und druckt sie aus.
  • _s3_ übergibt die ersten 3 Werte an _s4_ und bricht die Pipeline nach dem dritten Wert ab. Von _s3_ werden keine weiteren Werte erzeugt. Dies bedeutet nicht, dass keine weiteren Werte in der Pipeline sind. _s2_ würde immer noch mehr Werte erzeugen (und ausgeben), aber niemand fordert diese Werte an und die Ausführung wird gestoppt.
  • _s4_ überprüft erneut alle übergebenen Werte und druckt sie aus.
  • forEach verbraucht und druckt alles, was _s4_ an ihn weitergibt.

Denken Sie so darüber nach. Der ganze Strom ist völlig faul. Nur die Terminal-Operation zieht aktiv zieht ​​neue Werte aus der Pipeline. Nachdem 3 Werte von _s4 <- s3 <- s2 <- s1_ abgerufen wurden, erzeugt _s3_ keine neuen Werte mehr und es werden keine Werte mehr von _s2 <- s1_ abgerufen. Während _s1 -> s2_ weiterhin _4-9_ erzeugen kann, werden diese Werte niemals aus der Pipeline abgerufen und daher niemals von _s2_ gedruckt.

skip(6)

Mit skip() passiert dasselbe:

_Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.skip(6);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
_
  • _s1_ enthält 9 verschiedene Integer Werte.
  • _s2_ zeigt alle übergebenen Werte an und druckt sie aus.
  • _s3_ verbraucht die ersten 6 Werte, "überspringen", dh die ersten 6 Werte werden nicht an _s4_ übergeben, sondern nur die nachfolgenden Werte.
  • _s4_ überprüft erneut alle übergebenen Werte und druckt sie aus.
  • forEach verbraucht und druckt alles, was _s4_ an ihn weitergibt.

Wichtig ist hierbei, dass _s2_ nicht erkennt, dass die verbleibende Pipeline Werte überspringt. _s2_ zeigt alle Werte an, unabhängig davon, was danach passiert.

Ein anderes Beispiel:

Betrachten Sie diese Pipeline, die in diesem Blog-Beitrag aufgeführt ist

_IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);
_

Wenn Sie die oben genannten Schritte ausführen, wird das Programm niemals angehalten. Warum? Weil:

_IntStream i1 = IntStream.iterate(0, i -> ( i + 1 ) % 2);
IntStream i2 = i1.distinct();
IntStream i3 = i2.limit(10);

i3.forEach(System.out::println);
_

Was bedeutet:

  • _i1_ generiert eine unendliche Menge alternierender Werte: _0_, _1_, _0_, _1_, _0_, _1_,. ..
  • _i2_ verbraucht alle Werte, die zuvor angetroffen wurden, und gibt nur "new" Werte weiter, d. H. Es kommen insgesamt 2 Werte aus _i2_.
  • _i3_ gibt 10 Werte weiter und stoppt dann.

Dieser Algorithmus wird niemals anhalten, da _i3_ darauf wartet, dass _i2_ nach _0_ und _1_ weitere Werte erzeugt. Diese Werte werden jedoch niemals angezeigt, während _i1_ niemals stoppt die Eingabe von Werten an _i2_.

Es spielt keine Rolle, dass zu irgendeinem Zeitpunkt in der Pipeline mehr als 10 Werte produziert wurden. Wichtig ist nur, dass _i3_ diese 10 Werte noch nie gesehen hat.

Zur Beantwortung Ihrer Frage:

Ist es nur so, dass "jede Aktion vor dem Überspringen ausgeführt wird, während nicht alle vor dem Limit ausgeführt werden"?

Nee. Alle Operationen vor skip() oder limit() werden ausgeführt. In beiden Ausführungen erhalten Sie _A1_ - _A3_. Aber limit() kann die Pipeline kurzschließen und den Wertverbrauch abbrechen, sobald das Ereignis von Interesse (das Limit ist erreicht) aufgetreten ist.

11
Lukas Eder

Es ist vollkommene Blasphemie, Steam-Vorgänge einzeln zu betrachten, da ein Stream nicht so ausgewertet wird. 

Apropos limit (3) , es ist eine Kurzschlussoperation, die Sinn macht, weil das Nachdenken darüber, welche Operation vor und nach der limit ist, ein Limit in einem Stream hätte Stoppen Sie die Iteration, nachdem Sie n elements bis die Limit-Operation erhalten haben. Dies bedeutet jedoch nicht, dass nur n Stream-Elemente verarbeitet werden. Nehmen Sie diesen anderen Stream-Vorgang als Beispiel

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));
    }
}

würde ausgeben 

A1
A2B2C2
A3
A4B4C4
A5
A6B6C6

das scheint richtig zu sein, weil Limit darauf wartet, dass 3 Stream-Elemente die Operationskette durchlaufen, obwohl 6 Stream-Elemente verarbeitet werden.

8
Amm Sokun

Alle Streams basieren auf Spliteratoren, die im Wesentlichen zwei Vorgänge haben: vorwärts (ein Element vorwärts bewegen, ähnlich wie Iterator) und aufteilen (sich in beliebiger Position teilen, was für die parallele Verarbeitung geeignet ist). Sie können die Eingabe von Eingabeelementen jederzeit beenden (was limit ist), aber Sie können nicht einfach zu der beliebigen Position springen (es gibt keine solche Operation in der Spliterator-Schnittstelle). Die skip-Operation muss also tatsächlich die ersten Elemente aus der Quelle lesen, um sie zu ignorieren. Beachten Sie, dass Sie in einigen Fällen einen tatsächlichen Sprung ausführen können:

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

Vielleicht hilft dieses kleine Diagramm dabei, ein natürliches Gefühl dafür zu bekommen, wie der Stream verarbeitet wird.

Die erste Zeile =>8=>=7=...=== zeigt den Stream. Die Elemente 1..8 fließen von links nach rechts. Es gibt drei "Fenster":

  1. Im ersten Fenster (peek A) sehen Sie alles
  2. Im zweiten Fenster (skip 6 oder limit 3) wird eine Art Filterung durchgeführt. Entweder das erste oder das letzte Element werden "eliminiert" - dh nicht zur weiteren Verarbeitung weitergeleitet.
  3. Im dritten Fenster sehen Sie nur die Elemente, die weitergeleitet wurden

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

Wahrscheinlich ist nicht alles (vielleicht auch nicht alles) in dieser Erklärung technisch völlig korrekt. Aber wenn ich es so sehe, ist mir klar, welche Elemente welche der verketteten Anweisungen erreichen.

0
yaccob