Fortgeschrittene Techniken

Reihenfolge der Elemente im Stream

  • Die Reihenfolge der Elemente im Stream nennt man encounter order
    • Bei geordneten Collections übernimmt der Stream die Reihenfolge der Elemente aus der Collection (hat eine definierte encounter order)
    • Bei ungeordneten Collections haben die Elemente im Stream eine beliebige Reihenfolge (encounter order ist undefiniert)
  • Ist die encounter order definiert, haben die erzeugten Streams diese Ordnung
  • Die Ordnung kann über die unordered Methode abgeschaltet werden

Im Kapitel zu Collections wurde das Thema der Ordnung bereits behandelt. Während List eine Ordnung hält, ist das Set prinzipiell ungeordnet (mit Ausnahme des TreeSet, das ein SortedSet ist).

Dieselbe Überlegung muss man für Streams anstellen. Man kann zwar keine Elemente direkt im Stream ansprechen (daher der Name), man kann aber die Reihenfolge, mit der man die Elemente beim Durchlaufen des Streams antrifft (engl. encounter) notieren. Insofern kann man sich fragen, ob die encounter order eines Streams festgelegt (engl. defined) ist oder nicht.

Wenn man einen Stream aus einer Collection erzeugt, die geordnet ist, dann ist die encounter order des Streams definiert und entspricht der Ordnung der Collection. Diese wird über unterschiedliche Stream-Operationen erhalten und an erzeugte „Substreams“ weitergegeben – es sei denn, man ruft unordered() auf, worauf hin, die erzeugten Streams keine definierte encounter order mehr haben.

Ist die Collection aus der man den Stream erzeugt, selbst nicht geordnet, dann hat der Stream keine definierte encounter order. Es ist dann nicht vorhersagbar, welche Reihenfolge die Elemente eines „Substreams“ haben werden.

Collector

Typ der von der collect-Operation erzeugten Collection kann der toCollection()-Methode mitgegeben werden

Stream<String> stream = Stream.of("A", "B", "C");
List<String> list = stream.collect(toCollection(LinkedList::new));

Daten eines Streams können über Collector in einen einzigen Wert verdichtet werden

  • Durchschnitt: averagingDouble, averagingInt, averagingLong
  • Anzahl der Elemente: counting
  • Maximum und Minimum: maxBy, minBy
  • Summieren: summingInt, summingDouble, summingLong
Mittelwert berechnen
Stream<Double> stream = Stream.of(3.0, 2.0, 4.0, 1.0, 2.0, 2.7);
double average = stream.collect(averagingDouble(e -> e));
System.out.printf("%.2f%n", average); // -> 2,45
Summieren
Stream<Double> stream = Stream.of(3.0, 2.0, 4.0, 1.0, 2.0, 2.7);
double sum = stream.collect(summingDouble(e -> e));
System.out.printf("%.2f%n", sum); // -> 14,70

Daten aufteilen

Über den Collector partitioningBy und ein Predicate kann man einen Stream aufteilen. Das Ergebnis ist eine Map mit den beiden Schlüsseln Boolean.TRUE und Boolean.FALSE.

Stream<Character> stream = Stream.of('A', '2', 'X', '!', '3', '7');

Map<Boolean, List<Character>> result =
        stream.collect(partitioningBy(Character::isLetter));

List<Character> letters = result.get(true);
List<Character> other = result.get(false);

System.out.println(letters); // -> [A, X]
System.out.println(other); // -> [2, !, 3, 7]

Der Einrag in der Map für den Schlüssel true enthält alle Elemente, für die das Predicate true ergeben hat (hier Character::isLetter), also im Beispiel alle Buchstaben. Unter dem Schlüssel false sind die Elemente zu finden, für die das Ergebnis false war.

Gruppieren von Daten

Mit dem Collector groupingBy kann man Daten nach beliebigen Kriterien gruppieren. Hierzu muss eine Funktion übergeben werden, die das Gruppierungskriterium zurückgibt

Stream<String> stream = Stream.of("USW", "GGT", "UA", "ZB", "FKK",
        "MFG", "SOSE", "A", "H", "MM", "MBIT");

Map<Integer, List<String>> result =
        stream.collect(groupingBy(String::length));

System.out.println(result);
Ausgabe
{1=[A, H], 2=[UA, ZB, MM], 3=[USW, GGT, FKK, MFG], 4=[SOSE, MBIT]}

In diesem Beispiel werden die Strings in dem Stream nach ihrer Länge gruppiert. Die entsprechende Funktion zur Klassifizierung ist daher String::length. Der von der Funktion zurückgegebene Wert dient dann als Schlüssel in der Map, die die gruppierten Werte enthält.

Erzeugung von Strings

Soll aus einem Stream ein String erzeugt werden, kann man den joining Collector verwenden

Stream<String> stream = Stream.of("USW", "GGT", "UA", "ZB", "FKK",
        "MFG", "SOSE", "A", "H", "MM", "MBIT");

String result = stream.collect(joining(" | ", "{ ", " }"));

System.out.println(result);
Ausgabe
{ USW | GGT | UA | ZB | FKK | MFG | SOSE | A | H | MM | MBIT }

Man muss dem joining-Collector drei Informationen mitgeben:

  • Trennzeichen (delimiter)
  • Anfang (prefix)
  • Ende (suffix)

Parallelisierung

  • Stream-Operationen können automatisch parallelisiert werden
  • Anstatt stream() ruft man die Methode parallelStream()
  • Java VM kümmert sich um die Parallelisierung der Operationen
  • Lohnt sich erst ab einer gewissen Größe der Streams
  • Für den/die Programmierer:in ändert sich sonst nichts

In einem anderen Kapitel gehen wir noch auf das Thema Thread-Programmierung ein. Dort werden ausgiebig die Probleme behandelt, die durch die Parallelität entstehen und wie man ihnen begegnen kann.

Der große Vorteil des funktionalen Paradigmas, wie es bei den Streams eingesetzt wird, ist, dass man es automatisch parallelisieren kann, ohne dass Entwickler:innen etwas tun müssen. Der Grund liegt darin, dass die verwendeten Lambda-Funktionen seiteneffektfrei sind und damit kein gleichzeitiger Zugriff auf Daten durch die Funktionen erfolgen kann.

Details zu diesem Thema würden den Rahmen dieses Kurses sprengen. Es sei aber angemerkt, dass die funktionale Programmierung deutlich weniger Probleme bei der Parallelisierung erzeugt als die klassische imperative Programmierung und daher im Augenblick eine Renaissance erlebt.


Copyright © 2025 Thomas Smits