© DrHitch/Shutterstock.com
Java 9 Streams

2 Streams: Data in, Data out


Beginnen wir jetzt damit, uns die Schnittstelle zwischen Streams und der klassischen Java-Welt anzusehen.

2.1 Wo kommen die Quelldaten her?

Wenn man daran denkt, dass Streams nicht wie Collections ihre eigenen Daten halten, dann stellt sich die Frage, wo sie denn herkommen. Die gebräuchlichste Art, Streams zu erzeugen, ist die Verwendung von Methoden, die aus einer festen Anzahl von Elementen einen Stream erzeugen. Das sind die Methoden Stream.of(val1,val2,val3…) und Stream.of(array). Es gibt auch spezialisierte Streams für primitive Datentypen, die über spezifische Erzeugungsmethoden, zum Beispiel IntStream.range(int, int), verfügen. Zu den Methoden aus dem Bereich der Erzeugung aus einem festen Wertevorrat gehört auch die Methode, die aus einem String einen Stream erzeugt. Ein String ist nichts anderes als eine endliche Kette von chars. Sie kann als Wertevorrat übergeben werden:

final Stream<String> splitOf = Stream.of("A,B,C".split(","));

Streams können mithilfe der Methoden stream() und parallelStream()auch direkt aus Collections erzeugt werden. Den Unterschied von beiden Methoden werden wir uns zu einem späteren Zeitpunkt noch genauer ansehen. Auch Arrays bieten die Möglichkeit, direkt einen Stream aus ihnen zu erzeugen. Hierzu kann man die Methode Arrays.stream(..) verwenden.

Einige Klassen, die als Datenlieferanten dienen, haben Methoden, die einen Stream zurückliefern, zum Beispiel die Klasse Random mit der Methode Random.ints() oder BufferedReader.lines(). Genauso können Streams aus Streams erzeugt werden, was im Folgenden genauer dargestellt werden wird.

Nun fehlen noch zwei Möglichkeiten, Streams zu erzeugen. Mit einem Builder kann ein Stream programmatisch erzeugt werden:

final Stream<Pair> stream = Stream.<Pair>builder().add(new Pair()).build();

Die letzte Möglichkeit, einen Stream zu erzeugen, besteht darin, einen Generator zu verwenden. Das erfolgt über die Methode Streams.generate(..), in deren Argument die Methode eine Instanz der Klasse Supplier<T> bekommt (Listing 2.1).

Stream.generate(() -> {
final Pair p = new Pair();
p.id = random.nextInt(100);
p.value = "Value + " + p.id;
return p;
})

Listing 2.1

2.2 Wo gehen die Daten hin?

Nachdem wir jetzt wissen, woher die Daten kommen, stellt sich die Frage, wie die Daten aus dem Stream wiederzubekommen sind. Immerhin ist ja meist (nicht immer) angedacht, damit weiterzuarbeiten. Die einfachste Möglichkeit besteht darin, aus einem Stream mit der Methode stream.toArray() ein Array oder mittels stream.collect(Collectors.toList()) eine Liste zu erzeugen. Damit sind fast 90 Prozent der Einsatzgebiete beschrieben, aber es können auch Sets und Maps erzeugt werden.

Um als Ergebnis ein Set zu erhalten, kann mittels der Methode stream.collect(Collectors.toSet() ein HashSet angefordert werden. Ob es auch in den kommenden Versionen ein HashSet sein wird, ist nicht sicher. Maps hingegen werden mit stream.collect(Collectors.groupingBy(..)) erzeugt. Das Argument von groupingBy() sieht mindestens eine Funktion vor, mit der eine Gruppierung vorgenommen werden kann. Die Gruppierung stellt den Schlüssel in der Map dar, das Value ist dann eine Liste vom Typ der Elemente des Streams.

Eine für so manchen Entwickler etwas ungewohnte Möglichkeit besteht darin, den Stream in einem String auszugeben. Um das zu erreichen, wird in der Methode collect ein toStringJoiner verwendet, dessen Übergabeparameter ein Delimiter ist. Das Ergebnis ist dann eine Liste von durch diesen Delimiter konkatenierten toString()-Repräsentationen aller Elemente.

Nachfolgend einige Beispiele der gerade vorgestellten Methoden, ohne an dieser Stelle näher darauf einzugehen (Listing 2.2). Wir werden uns alles nach und nach im Detail ansehen.

public static void main(String[] args) {
final List<Pair<String,String>> generateDemoValues
= generateDemoValues();
//Stream from Values
final Stream<String> fromValues
= Stream.of("A" , "B");
//Stream from Array
final String[] strings = {"A" , "B"};
final Stream<String> fromArray = Stream.of(strings);
//Stream from List
final Stream<Pair<String,String>> fromList
= generateDemoValues.stream();
//Stream from String
final Stream<String> abc = Stream.of("ABC");
final Stream<IntStream> of = Stream.of("ABC".chars());
final Stream<String> splitOf
= Stream.of("A,B,C".split(","));
//Stream from builder
final Stream<String> builderPairStream =
Stream.<String>builder().add("A").build();
//Stream to Array
final Pair<String, String>[] toArray =
generateDemoValues.stream().toArray(Pair[]::new);
//Stream to List
final List<Pair> toList =
generateDemoValues.stream().collect(Collectors.toList());
//Stream to Set
final Set<Pair> toSet =
generateDemoValues.stream().collect(Collectors.toSet());
//Stream to Map
final Map<String, List<Pair<String,String>>> collectedToMap =
generateDemoValues
.stream()
.collect(Collectors
.groupingBy(Pair::getT1));
System.out.println("collectedToMap.size() = " + collectedToMap.size());

for (final Map.Entry<String, List<Pair<String, String>>> entry :
collectedToMap.entrySet()) {
System.out.println("entry = " + entry);
}
}

private static List<Pair<String,String>> generateDemoValues() {
return Arrays.asList(
new Pair<>("A" , "A") ,
new Pair<>("B" , "B") ,
new Pair<>("C" , "C")
);
}

Listing 2.2

2.3 Collectors

Wir haben bisher die Klasse Collectors verwendet, ohne sie uns genauer anzusehen. Hier finden sich eine Menge Methoden, die eine Implementierung des Interface java.util.stream.Collector liefern. Das Interface Collector selbst ist hinsichtlich der Anzahl der definierten Methoden noch recht überschaubar, die Implementierung kann jedoch ein wenig aufwendiger und komplexer werden. Zu Beginn werden wir damit vorlieb nehmen, die Service Method der Klasse Collectors zu verwenden.

2.3.1 toList und toSet

Die wohl gebräuchlichsten und meiner Meinung nach einfachsten Methoden haben wir schon kennen gelernt. Hierbei wird aus der Ergebnismenge des Streams eine Liste oder ein Set erzeugt. Auch hier gibt es noch einige Feinheiten, die man eventuell gebrauchen könnte.

Wenn wir die Methoden ohne die explizite Angabe einer Implementierung verwenden, bekommen wir im Fall einer Liste die Implementierung der ArrayList und bei einem Set die Implementierung HashSet. Wenn wir nun eine LinkedList haben wollen, gehen wir den in Listing 2.3 gezeigten Weg.

public static Stream<Pair<String, String>> nextStream() {
return Stream.of(
new Pair<>("A" , "A") ,
new Pair<>("B" , "B") ,
new Pair<>("C" , "C")
);
}

public static void main(String[] args) {

List<Pair<String,String>> listA = nextStream()
.collect(Collectors.toCollection(()-> new LinkedList<>()));

List<Pair<String,String>> listB = nextStream()
.collect(Collectors.toCollection(LinkedList::new));
}

Listing 2.3

In Listing 2.3 wird mithilfe der Methode Collectors.toCollection(..) eine Factory eingesetzt, die dazu verwendet wird, die gewünschte Instanz einer Collection zu erzeugen. Dabei habe ich als Beispiel die LinkedList verwendet.

An dieser Stelle möchte ich auf die Interfaces und Klassen im Package utils hinweisen. Dorthin werde ich die öfter benötigten Methoden, zum Beispiel die Methode zum Erzeugen von Beispieldaten oder Streams, auslagern.
In den nachfolgenden Quelltexten werden diese Implementierungen im Allgemeinen nicht mehr gezeigt, um die Beispiele nicht unnötig redundant werden zu lassen.

2.3.2 toMap

Möchte man eine Map als Datenstruktur erhalten, kann man mit der Methode toMap(..) arbeiten. Diese gibt es in drei Ausprägungen.

Beginnen wir mit der ersten, die uns die Möglichkeit gibt, zwei Funktionen anzugeben. Die erste ist eine Funktion vom Datentyp auf den gewünschten Schlüssel der Map und die zweite eine Funktion vom Datentyp auf den gewünschte Wert zu dem gerade erzeugten Schlüssel. Somit sind Key und Value eine Funktion basierend auf dem Datentyp des Value, das sich an dieser Stelle gerade im Stream befindet. In Listing 2.4 bin ich sehr ausführlich vorgegangen und werde in Listing 2.5 die Implementierung mittels Lambdas und Methodenreferenzen ein wenig kompakter gestalten.

final Map<String, String> map = nextStream()
.collect(Collectors.toMap(new Function<Pair<String, String>, String>() {
@Override
public String apply(Pair<String, String> p) {
return p.getT1();
}
} , new Function<Pair<String, String>, String>() {
@Override
public String apply(Pair<String, String> p) {
return p.getT2().toLowerCase();
}
}));
System.out.println("map = " + map);

Listing 2.4

final Map<String, String> mapB = nextStream()
.collect(Collectors.toMap(Pair::getT1 , p -> p.getT2().toLowerCase()));

System.out.println("mapB = " + mapB);

Listing 2.5

Wir bekommen von der Klasse HashMap eine Instanz zurückgeliefert. Wichtig ist dabei zu wissen, dass eine Fehlermeldung erzeugt wird, wenn wir einen Schlüssel zweimal erhalten. Wenn wir in unserem Fall die Methode nextStream() intern ändern und die Methode DemoData.nextStreamWithDuplicates()verwenden, bekommen wir bei der ersten Implementierung folgende Fehlermeldung:

IllegalStateException: Duplicate key B (attempted merging values b and b)

Sehen wir uns nun die zweite Methodensignatur von toMap(..) an. Hier können wir zusätzlich BinaryOperator<T> extends BiFunction<T,T,T> angeben, was es uns ermöglicht, im Fall eines Konflikts eine explizite Entscheidung zu treffen (Listing 2.6).

final Map<String, String> mapC = nextStream()
.collect(Collectors.toMap(Pair::getT1 ,
p -> p.getT2().toLowerCase() ,
new BinaryOperator<String>() {
@Override
public String apply(String s1 , String s2) {
return s1 + " - " + s2;
}
}

));
System.out.println("mapC = " + mapC);

Listing 2.6

Listing 2.7 zeigt das in kompakter Form und unter Verwendung von statischen Imports.

final Map<String, String> mapB = nextStream()
.collect(toMap(Pair::getT1 ,
p -> p.getT2().toLowerCase() ,
(s1 , s2) -> s1 + " - " + s2));

System.out.println("mapB = " + mapB);

Listing 2.7

Schaut man sich nun die Ausgabe an, sieht man, dass die Values bei mehrfach auftretenden Schlüsseln einfach hintereinander gehängt werden:

mapB = {A=a, B=b - b, C=c - c - c}

Kommen wir nun zur dritten Variante, bei der zusätzlich noch ein Supplier angegeben werden kann, mit dem die zu verwendende Datenstruktur erzeugt wird. Hier kann an wie im nachfolgenden Fall eine TreeMap oder natürli...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang