1. Stream.concat: zwei Streams zusammenführen
In der Programmierung tritt oft die Situation auf: Wir haben zwei (oder mehr) Daten-Streams und möchten sie zu einem zusammenführen. Zum Beispiel zwei Listen von Studierenden aus verschiedenen Gruppen – und wir müssen alle auf einmal verarbeiten. In „früheren Zeiten“ (vor dem Stream API) hätten wir einfach zwei Listen mit addAll zusammengeführt. Arbeiten wir jedoch mit Streams (Stream<T>), wollen wir das lazy und ausdrucksstark tun.
Syntax und Funktionsweise
Die grundlegendste Möglichkeit, zwei Streams zu vereinen, ist die Verwendung der statischen Methode Stream.concat:
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
Was passiert:
- Zuerst wird der erste Stream, danach der zweite konsumiert.
- Am Ende erhalten wir einen neuen Stream, der zunächst alle Elemente des ersten und anschließend alle des zweiten liefert.
- Das Zusammenführen ist lazy (träge): Solange Sie den resultierenden Stream nicht mit einer terminalen Operation (z. B. forEach oder collect) traversieren, geschieht nichts.
Beispiel: zwei Namenslisten zusammenführen
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris", "Vika");
List<String> groupB = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());
allStudents.forEach(System.out::println);
Ergebnis:
Anya
Boris
Vika
Grisha
Dasha
Wichtig: Der resultierende Stream ist nach dem Zusammenführen einmalig. Wie jeder andere Stream kann er nicht erneut verwendet werden.
Besonderheiten von Stream.concat
- Nur zwei Streams auf einmal. Für drei oder mehr – entweder verkettet mit concat oder andere Ansätze (siehe unten).
- Reihenfolge bleibt erhalten. Zuerst die Elemente des ersten Streams, dann die des zweiten.
- Lazy-Zusammenführung. Ist der erste Stream unendlich, wird der zweite nie erreicht.
- Typischer Bug: Der Versuch, einen Stream mit sich selbst zu vereinen (Stream.concat(stream, stream)) führt zur Wiederverwendung desselben Streams – das ist verboten.
2. Mehr als zwei Streams zusammenführen: flatMap und Stream.of
Wenn es mehr als zwei Streams sind, ist die Kombination Stream.of + flatMap oft bequemer.
Verwendung von Stream.of + flatMap
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris");
List<String> groupB = List.of("Vika");
List<String> groupC = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
.flatMap(Collection::stream);
allStudents.forEach(System.out::println);
Was hier passiert:
- Stream.of(groupA, groupB, groupC) erstellt einen Stream aus drei Collections.
- flatMap(Collection::stream) klappt jede Collection in einen gemeinsamen String-Stream auf.
- Resultat: ein großer Stream aller Studierenden.
Pluspunkt: Sie können beliebig viele Streams zusammenführen.
Weitere Variante: eine Liste von Streams zusammenführen
import java.util.List;
import java.util.stream.Stream;
List<Stream<String>> streams = List.of(
Stream.of("a", "b"),
Stream.of("c"),
Stream.of("d", "e")
);
Stream<String> merged = streams.stream()
.flatMap(s -> s);
merged.forEach(System.out::println);
Ergebnis:
a
b
c
d
e
3. Collectors.joining: Zeichenketten mit Trennzeichen zusammenfügen
Manchmal müssen wir nicht nur Streams verschmelzen, sondern alle Elemente zu einer einzigen Zeichenkette zusammenfügen – zum Beispiel, um Namen kommagetrennt auszugeben. Dafür gibt es den Collector Collectors.joining.
Syntax
String result = stream.collect(Collectors.joining(", "));
- Ohne Argumente: fügt Strings ohne Trennzeichen zusammen.
- Mit Trennzeichen: zwischen den Elementen steht die angegebene Zeichenkette.
- Mit Präfix und Suffix: Collectors.joining(delimiter, prefix, suffix)
Beispiel: Liste der Studierenden als eine Zeichenkette
import java.util.List;
import java.util.stream.Collectors;
List<String> students = List.of("Anya", "Boris", "Vika");
String line = students.stream()
.collect(Collectors.joining(", "));
System.out.println("Liste der Studierenden: " + line);
Ergebnis:
Liste der Studierenden: Anya, Boris, Vika
Beispiel mit Präfix und Suffix
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
Ergebnis:
[Anya, Boris, Vika]
Wofür ist das nützlich?
- CSV-Zeilen erzeugen.
- Schöne Ausgabe von Listen und Reports.
- Übergabe von Daten als eine Zeichenkette (z. B. in einer URL oder im Log).
4. Vergleich von concat und flatMap: wann verwendet man was?
- Stream.concat – wenn es genau zwei Streams gibt und die Reihenfolge wichtig ist.
- Stream.of + flatMap – wenn es viele Streams sind oder sie in einer Collection liegen.
- Collectors.joining – wenn die Elemente vom Typ String sind und das Ergebnis eine einzelne Zeichenkette sein soll.
Vergleichstabelle
| Methode | Wann verwenden | Codebeispiel |
|---|---|---|
|
Zwei Streams | |
|
Viele Streams, Collection von Streams | |
|
Elemente zu einer Zeichenkette zusammenfügen | |
5. Praktische Beispiele in einer Lernanwendung
Beispiel 1. Studierende aus zwei Fakultäten zusammenführen
import java.util.List;
import java.util.stream.Stream;
List<String> itStudents = List.of("Anya", "Boris");
List<String> mathStudents = List.of("Vika", "Grisha");
Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());
all.forEach(System.out::println);
Beispiel 2. Studierende aus allen Gruppen zusammenführen und als eine Zeichenkette ausgeben
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
List<List<String>> allGroups = List.of(
List.of("Anya", "Boris"),
List.of("Vika"),
List.of("Grisha", "Dasha")
);
String allNames = allGroups.stream()
.flatMap(Collection::stream)
.collect(Collectors.joining("; "));
System.out.println("Alle Studierenden: " + allNames);
Ergebnis:
Alle Studierenden: Anya; Boris; Vika; Grisha; Dasha
Beispiel 3. Zahlen-Streams zusammenführen
import java.util.stream.Stream;
Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);
Stream<Integer> merged = Stream.concat(s1, s2);
merged.forEach(System.out::print); // 12345
6. Wichtige Feinheiten und Besonderheiten
concat funktioniert nur mit zwei Streams
Für drei Streams ergibt sich eine geschachtelte Schreibweise – das funktioniert, ist aber unübersichtlich. Besser Stream.of + flatMap verwenden:
Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);
Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);
Streams sind einmalig
Nach einer terminalen Operation kann ein Stream nicht wiederverwendet werden. Eine erneute Verwendung führt zu IllegalStateException.
Der durch concat erzeugte Stream ist lazy
Wenn der erste Stream unendlich ist (z. B. erstellt mit Stream.generate(...)), wird der zweite niemals erreicht.
Reihenfolge der Elemente
Die Reihenfolge bleibt immer erhalten: zuerst der erste Stream, dann der zweite.
Collectors.joining funktioniert nur mit Stream<String>
Für Nicht-Strings wandeln Sie die Elemente zuerst in Strings um, z. B. via map(Object::toString):
import java.util.stream.Collectors;
import java.util.stream.Stream;
Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.println(line); // 1, 2, 3
Vergleich mit „manuellem“ Ansatz
Vor dem Stream API:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
Mit dem Stream API:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
Vorteil von Streams: Sie können Zwischenoperationen (filter, map u. a.) vor und nach dem Zusammenführen einbauen und mit beliebigen Datenquellen arbeiten.
7. Typische Fehler beim Zusammenführen von Streams
Fehler Nr. 1: denselben Stream erneut verwenden. Ein Stream darf nur einmal verwendet werden. Man darf einen Stream nicht mit sich selbst zusammenführen.
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Fehler! s wird erneut verwendet
Fehler Nr. 2: Versuch, einen unendlichen Stream mit einem endlichen zu vereinen. Ist der erste Stream unendlich, beginnt der zweite nie.
import java.util.stream.Stream;
Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);
Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // finite wird nicht im Ergebnis erscheinen
Fehler Nr. 3: joining für Nicht-Strings. Der Collector Collectors.joining akzeptiert einen Stream von Strings. Für Zahlen müssen Sie vorher in Strings umwandeln:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Kompilierfehler!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
Fehler Nr. 4: Verletzung der Reihenfolge. Wenn die Reihenfolge wichtig ist, verwenden Sie concat oder ein geordnetes flatMap. Methoden wie unordered() können die deterministische Ausgabereihenfolge aufheben.
GO TO FULL VERSION