Java 8 und Infinite Streams

1. Übersicht

In diesem Artikel werden wir uns eine java.util.Stream- API ansehen und sehen, wie wir dieses Konstrukt verwenden können, um mit einem unendlichen Strom von Daten / Elementen zu arbeiten.

Die Möglichkeit, an der unendlichen Folge von Elementen zu arbeiten, beruht auf der Tatsache, dass Streams so aufgebaut sind, dass sie faul sind.

Diese Faulheit wird durch eine Trennung zwischen zwei Arten von Operationen erreicht, die in Streams ausgeführt werden könnten: Zwischen- und Terminaloperationen .

2. Zwischen- und Terminalbetrieb

Alle Stream- Vorgänge sind in Zwischen- und Terminal- Vorgänge unterteilt und werden zu Stream-Pipelines zusammengefasst.

Eine Stream-Pipeline besteht aus einer Quelle (z. B. einer Sammlung , einem Array, einer Generatorfunktion, einem E / A-Kanal oder einem Generator für unendliche Sequenzen). gefolgt von null oder mehr Zwischenoperationen und einer Terminaloperation.

2.1. Zwischen Operationen

Zwischenoperationen werden nicht ausgeführt, wenn eine Terminaloperation aufgerufen wird.

Sie bilden eine Pipeline einer Stream- Ausführung. Die Zwischenoperation kann einer Stream- Pipeline mit folgenden Methoden hinzugefügt werden:

  • Filter()
  • Karte()
  • flatMap ()
  • deutlich ()
  • sortiert ()
  • spähen()
  • Grenze()
  • überspringen()

Alle Zwischenoperationen sind faul, daher werden sie erst ausgeführt, wenn tatsächlich ein Ergebnis einer Verarbeitung benötigt wird.

Grundsätzlich geben Zwischenoperationen einen neuen Stream zurück. Das Ausführen einer Zwischenoperation führt tatsächlich keine Operation aus, sondern erstellt stattdessen einen neuen Stream, der beim Durchlaufen die Elemente des anfänglichen Streams enthält, die mit dem angegebenen Prädikat übereinstimmen.

Daher beginnt das Durchlaufen des Streams erst, wenn der Terminalbetrieb der Pipeline ausgeführt wird.

Dies ist eine sehr wichtige Eigenschaft, die insbesondere für unendliche Streams wichtig ist, da damit Streams erstellt werden können, die nur dann aufgerufen werden, wenn eine Terminaloperation aufgerufen wird.

2.2. Terminal Operationen

Terminaloperationen können den Stream durchlaufen, um ein Ergebnis oder eine Nebenwirkung zu erzielen.

Nachdem der Terminalbetrieb ausgeführt wurde, wird die Stream-Pipeline als verbraucht betrachtet und kann nicht mehr verwendet werden. In fast allen Fällen sind die Terminaloperationen eifrig und durchlaufen die Datenquelle und die Verarbeitung der Pipeline, bevor sie zurückkehren.

Der Eifer einer Terminaloperation ist wichtig für unendliche Streams, da wir zum Zeitpunkt der Verarbeitung sorgfältig überlegen müssen, ob unser Stream beispielsweise durch eine limit () - Transformation ordnungsgemäß begrenzt ist . Terminalbetrieb sind:

  • für jeden()
  • forEachOrdered ()
  • toArray ()
  • reduzieren()
  • sammeln()
  • Mindest()
  • max ()
  • Anzahl()
  • anyMatch ()
  • allMatch ()
  • noneMatch ()
  • findFirst ()
  • finde irgendein()

Jede dieser Operationen löst die Ausführung aller Zwischenoperationen aus.

3. Unendliche Streams

Nachdem wir diese beiden Konzepte - Zwischen- und Terminaloperationen - verstanden haben, können wir einen unendlichen Stream schreiben, der die Faulheit von Streams nutzt.

Angenommen, wir möchten einen unendlichen Strom von Elementen aus Null erstellen, der um zwei erhöht wird. Dann müssen wir diese Sequenz begrenzen, bevor wir den Terminalbetrieb aufrufen.

Es ist wichtig, eine limit () -Methode zu verwenden, bevor Sie eine collect () -Methode ausführen , bei der es sich um eine Terminaloperation handelt. Andernfalls wird unser Programm auf unbestimmte Zeit ausgeführt:

// given Stream infiniteStream = Stream.iterate(0, i -> i + 2); // when List collect = infiniteStream .limit(10) .collect(Collectors.toList()); // then assertEquals(collect, Arrays.asList(0, 2, 4, 6, 8, 10, 12, 14, 16, 18));

Wir haben einen unendlichen Stream mit einer iterate () -Methode erstellt. Dann haben wir eine limit () - Transformation und eine collect () - Terminaloperation aufgerufen . Dann haben wir in unserer resultierenden Liste die ersten 10 Elemente einer unendlichen Sequenz aufgrund der Faulheit eines Streams.

4. Unendlicher Strom eines benutzerdefinierten Elementtyps

Angenommen, wir möchten einen unendlichen Strom zufälliger UUIDs erstellen .

Der erste Schritt, um dies mithilfe der Stream- API zu erreichen, besteht darin, einen Lieferanten mit diesen zufälligen Werten zu erstellen :

Supplier randomUUIDSupplier = UUID::randomUUID;

Wenn wir einen Lieferanten definieren, können wir mit einer generate () -Methode einen unendlichen Stream erstellen :

Stream infiniteStreamOfRandomUUID = Stream.generate(randomUUIDSupplier);

Then we could take a couple of elements from that stream. We need to remember to use a limit() method if we want our program to finish in a finite time:

List randomInts = infiniteStreamOfRandomUUID .skip(10) .limit(10) .collect(Collectors.toList());

We use a skip() transformation to discard first 10 results and take the next 10 elements. We can create an infinite stream of any custom type elements by passing a function of a Supplier interface to a generate() method on a Stream.

6. Do-While – the Stream Way

Let's say that we have a simple do..while loop in our code:

int i = 0; while (i < 10) { System.out.println(i); i++; }

We are printing i counter ten times. We can expect that such construct can be easily written using Stream API and ideally, we would have a doWhile() method on a stream.

Unfortunately, there is no such method on a stream and when we want to achieve functionality similar to standard do-while loop we need to use a limit() method:

Stream integers = Stream .iterate(0, i -> i + 1); integers .limit(10) .forEach(System.out::println);

We achieved same functionality like an imperative while loop with less code, but call to the limit() function is not as descriptive as it would be if we had a doWhile() method on a Stream object.

5. Conclusion

In diesem Artikel wird erläutert, wie wir mit der Stream-API unendliche Streams erstellen können. Wenn diese zusammen mit Transformationen wie limit () verwendet werden, können einige Szenarien leichter zu verstehen und zu implementieren sein.

Der Code, der all diese Beispiele unterstützt, befindet sich im GitHub-Projekt - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.