JAVA Tutorial#70

Streams zur Massendatenverarbeitung

2024-05-28 | credits: Cevko@stock.adobe

Overview

Ein Stream in Java ist eine Daten-Sequenz, auf die Operationen wie Filtern, Sortieren oder Transformieren funktional angewendet werden können. Wir können uns einen Stream wie eine Art Fließand in einer Fabrik vorstellen, auf dem ebenfalls unterschiedliche Verarbeitungsschritte nacheinander abgearbeitet werden.

Was ist ein Stream?

In der Vergangenheit haben wir bereits regelmäßig mit Datensammlungen aus dem Collections-Framework gearbeitet, wobei wir unsere Aufgaben objektorientiert gelöst haben. 

Gerade bei sehr großen Datenmengen (Big Data) ist es aber entscheidend, entsprechende Programmieraufgaben möglichst effizient, elegant und performant lösen zu können. Hier kommt das neu einführte Konzept der Streams in Spiel. Damit nämlich lässt sich die Massendatenverarbeitung auf Collections (Bulk Operations on Collections) mit Mustern der der funktionalen Programmierung lösen. 

Der Performance-Vorteil von Streams gegenüber der traditionellen (objektorientierten) Arbeit mit Collections kommt daher, da Streams kaum bzw. keine Daten zwischenspeichern und dadurch deutlich weniger Speicher verbrauchen. 

Wie funktioniert ein Stream?

Grundlage für Streams ist das Interface java.util.stream.Stream<T>. Doch was genau ist ein Stream?

  • Ein Stream ist eine Folge von Elementen (Daten), die aus einer bestehenden Collection (die Datenquelle) bezogen werden.
  • Auf dem Stream können verschiedene, oftmals auch verkettete, Operationen (Chaining) durchgeführt werden. 
  • Streams können keine Speicherung der Daten vornehmen und alle Operationen können nur einmal traversiert (durchlaufen) werden.

Manche vergleichen Streams mit der Abarbeitung am Fließband, und das ergibt durchaus Sinn. Grafisch lässt sich das Konzept der Streams so darstellen: 

Infografik Java Streams

Es gibt drei Arten von Operationen:

  • create Operation: Erzeugt den Stream aus einer bestehenden Datenquelle
  • Intermediate Operation: Diese Operationen liefern als Rückgabewert wieder einen Stream, auf dem man wieder eine Stream-Operation ausführen kann. Somit lassen sich ganze Operationen-Ketten durchführen ("Chaining"). 
  • Terminal Operationl: Diese Operationen leifern keinen Stream als Rückgabewert. Da Streams nur einmal verwendet werden können, ist der Stream nach einer terminalen Operation nicht mehr verfügbar und die Kette bricht ab. 

Intermediate Operations sind als "Black Box" zu sehen. Wir interessieren uns nur dafür, was am Ende rauskommt. Was innerhalb der einzelnen Operationen passiert, ist ein Detail der Implementierung. 

Einen Stream erzeugen

Das Stream Interface befindet sich im java.util.stream Paket. Mittels der Methode steam() lässt sich aus einer Collection ein Stream-Objekt erstellen: 

List<String> list = Arrays.asList("Wolf", "Tiger", "Drache");
Stream<String> stream = list.stream(); // sequentieller Stream

In unserem Beispiel haben wir einen sequentiellen Stream aus der Liste erzeugt. Wir können jedoch mittels der Methode parallelStream() alternativ auch einen parallelen Stream erstellen: 

Stream<String> stream = list.parallelStream(); // paralleler Stream

Ein Stream in Parallelverarbeitung hat den Vorteil, dass die Elemente parallel verarbeitet werden: Es ist so, als würden auf unserem Fließband Arbeitsschritte für mehrere Elemente nicht sequentiell (nacheinander), sondern gleichzeitig durchgeführt werden. Gerade bei großen Streams kann das einen erheblichen Performance-Gewinn bedeuten. 

Wir sollten für kleinere Streams aber die sequentielle "Standard"-Variante verwenden. Sequentielle Streams haben nämlich in der Regel weniger Overhead Cost, da keine zusätzliche Rechenleistung für die Koordinierung der Paralellverarbeitung erforderlich ist. Mit anderen Worten: Kleine Streams laufen sequentiell schneller. 

Einen Stream verwenden

Da wir nun wissen, was ein Stream ist und wie wir ihn erstellen, wollen wir uns in zwei einfachen Beispiel mit der Benutzung vertraut machen.

Beispiel 1:

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 6);
int summe = numbers.stream().filter(p -> p % 2 == 0).mapToInt(p->p).sum(); // 12

Als Datenquelle für unseren Stream dient eine Liste aus ganzen Zahlen. Als Ergebnis möchten wir die Summe aller geraden Zahlen der Liste in einer int-Variable speichern. 

  1. Die Methode stream() ist unsere create Operation und liefert uns aus unserer Liste numbers ein Stream-Objekt zurück, mit dem wir nun weiterarbeiten können (beachte: die "Original"-Liste wird nicht verändert). 
  2. Auf dem Stream wenden wir nun die filter()-Methode an. Diese erwartet einen Lambda-Ausdruck vom Typs Predicate. Wir haben Predicate so implementiert, dass true geliefert wird, wenn der jeweilige Zahlenwert gerade ist. 
  3. Nun erzeugen wir mit mapToInt() einen IntStream, da nur dieser über die Methode sum() verfügt. 
  4. Mit der Methode sum() endet unser Chaining (Terminal Operation), da diese keinen Stream zurückliefert, sondern einen konkreten int-Wert. 

Beispiel2: 

Im nächsten Beispiel schauen wir uns einen weiteren Stream an, der wieder mit der Methode stream aus einer Liste von Player-Objekten erzeugt wird. Im Chaining wird der Stream dann mit der Methode mehrAls100Punkte gefiltert wird und anschließend wieder als List<Player> zurückgegeben: 

List<Player> guteSpieler = spieler.stream().                 // Create
                           filter(Player::mehrAls100Punkte). // Intermediate
                           collect(Collectors.toList());     // Terminal

Fazit

Wir haben gesehen, wie effizient die Verkettung von Stream-Operationen (Intermediate Operations) ist und um wieviel klarer unser Code wird. Lösungen mit Streams sind ein wunderbares Beispiel dafür, wie sinnvoll das Konzept des "Was" (funktionale Programmierung) gegenüber dem "Wie" sein kann. 

Im nächsten Tutorial gehen wir dann genauer auf die einzelnen Intermediate und Terminal Operations ein

Werbung

Java lernen

Werde zum Java Profi!

PHP Lernen

Lerne serverbasierte Programmierung

JavaScript lernen

Skille dein Webcoding

FALCONBYTE.NET

Handmade with 🖤️

© 2018-2023 Stefan E. Heller

Impressum | Datenschutz | Changelog

Falconbyte Youtube Falconbyte GitHub facebook programmieren lernen twitter programmieren lernen discord programmieren lernen