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
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
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);
{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);
{ 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 MethodeparallelStream()
- 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.