1. Stream.concat: łączymy dwa strumienie
W programowaniu często pojawia się sytuacja: mamy dwa (lub więcej) strumienie danych i chcemy je połączyć w jeden. Na przykład dwie listy studentów z różnych grup — i trzeba przetworzyć wszystkich naraz. W „starych czasach” (przed Stream API) po prostu połączylibyśmy dwie listy przy pomocy addAll. Ale jeśli pracujemy ze strumieniami (Stream<T>), chcemy zrobić to leniwie i wyraziście.
Składnia i zasada działania
Najprostszy sposób połączyć dwa strumienie — użyć statycznej metody Stream.concat:
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
Co się dzieje:
- Najpierw brany jest pierwszy strumień, następnie drugi.
- Na wyjściu — nowy strumień, który najpierw zwraca wszystkie elementy z pierwszego, a potem wszystkie elementy z drugiego.
- Łączenie jest leniwe: dopóki nie zaczniesz konsumować wynikowego strumienia operacją terminalną (np. forEach lub collect), nic się nie stanie.
Przykład: łączenie dwóch list imion
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);
Wynik:
Anya
Boris
Vika
Grisha
Dasha
Ważne: po połączeniu wynikowy strumień jest jednorazowy. Jak każdy inny Stream, nie można go użyć ponownie.
Cechy Stream.concat
- Tylko dwa strumienie naraz. Dla trzech i więcej — albo łańcuchem concat, albo inne podejścia (zob. niżej).
- Zachowany jest porządek. Najpierw elementy pierwszego strumienia, potem drugiego.
- Leniwe łączenie. Jeśli pierwszy strumień jest nieskończony, do drugiego wykonanie nigdy nie dojdzie.
- Typowy błąd: próba połączenia strumienia z samym sobą (Stream.concat(stream, stream)) prowadzi do ponownego użycia tego samego strumienia — to jest niedozwolone.
2. Łączenie więcej niż dwóch strumieni: flatMap i Stream.of
Gdy strumieni jest więcej niż dwa, wygodniej zebrać je kombinacją Stream.of + flatMap.
Używamy 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);
Co tu się dzieje:
- Stream.of(groupA, groupB, groupC) tworzy strumień z trzech kolekcji.
- flatMap(Collection::stream) spłaszcza każdą kolekcję do jednego strumienia napisów.
- Efekt — jeden duży strumień wszystkich studentów.
Plus: można łączyć dowolną liczbę strumieni.
Jeszcze wariant: łączenie listy strumieni
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);
Wynik:
a
b
c
d
e
3. Collectors.joining: łączymy napisy z separatorem
Czasem trzeba nie tylko scalić strumienie, ale skleić wszystkie elementy w jeden łańcuch — na przykład, aby wypisać imiona po przecinku. Do tego służy kolektor Collectors.joining.
Składnia
String result = stream.collect(Collectors.joining(", "));
- Bez argumentów: sklei napisy bez separatora.
- Z separatorem: między elementami pojawi się podany łańcuch.
- Z prefiksem i sufiksem: Collectors.joining(delimiter, prefix, suffix)
Przykład: lista studentów w jeden łańcuch
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("Lista studentów: " + line);
Wynik:
Lista studentów: Anya, Boris, Vika
Przykład z prefiksem i sufiksem
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
Wynik:
[Anya, Boris, Vika]
Po co to?
- Tworzenie wierszy CSV.
- Ładny wydruk list i raportów.
- Przekazanie danych w jednym łańcuchu (np. w URL lub logu).
4. Porównanie concat i flatMap: kiedy czego używać?
- Stream.concat — gdy są dokładnie dwa strumienie i ważne jest zachowanie kolejności.
- Stream.of + flatMap — gdy strumieni jest dużo lub są przechowywane w kolekcji.
- Collectors.joining — gdy elementy mają typ String i potrzebny jest wynik w postaci jednego łańcucha.
Tabela porównawcza
| Metoda | Kiedy używać | Przykład kodu |
|---|---|---|
|
Dwa strumienie | |
|
Wiele strumieni, kolekcja strumieni | |
|
Skleić elementy w jeden łańcuch | |
5. Praktyczne przykłady w aplikacji edukacyjnej
Przykład 1. Łączymy studentów z dwóch wydziałów
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);
Przykład 2. Łączymy studentów ze wszystkich grup, wypisujemy w jeden łańcuch
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("Wszyscy studenci: " + allNames);
Wynik:
Wszyscy studenci: Anya; Boris; Vika; Grisha; Dasha
Przykład 3. Łączymy strumienie liczb
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. Ważne niuanse i cechy szczególne
concat działa tylko z dwoma strumieniami
Dla trzech strumieni wyjdzie zagnieżdżony zapis — działa, ale wygląda hałaśliwie. Lepiej użyć Stream.of + flatMap:
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);
Strumienie są jednorazowe
Po operacji terminalnej strumienia nie można użyć ponownie. Ponowne użycie prowadzi do IllegalStateException.
Strumień concat jest leniwy
Jeśli pierwszy strumień jest nieskończony (np. utworzony przez Stream.generate(...)), do drugiego wykonanie nigdy nie dojdzie.
Kolejność elementów
Kolejność zawsze jest zachowana: najpierw pierwszy strumień, potem drugi.
Collectors.joining działa tylko z Stream<String>
Dla nie-łańcuchów najpierw przekształć elementy do napisów, np. przez 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
Porównanie z „ręcznym” podejściem
Przed Stream API:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
Z Stream API:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
Zaleta strumieni: można włączać operacje pośrednie (filter, map i inne) przed i po łączeniu, a także pracować z dowolnymi źródłami danych.
7. Typowe błędy przy łączeniu strumieni
Błąd nr 1: ponowne użycie strumienia. Strumienia można użyć tylko raz. Nie wolno łączyć strumienia z samym sobą.
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Błąd! s zostanie użyty ponownie
Błąd nr 2: próba połączenia strumienia nieskończonego ze skończonym. Jeśli pierwszy strumień jest nieskończony, drugi nigdy się nie rozpocznie.
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 nie trafi do wyniku
Błąd nr 3: joining dla nie-łańcuchów. Kolektor Collectors.joining przyjmuje strumień napisów. Dla liczb trzeba wcześniej przekształcić do napisów:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Błąd kompilacji!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
Błąd nr 4: naruszenie kolejności. Jeśli kolejność jest ważna, używaj concat albo uporządkowanego flatMap. Metody typu unordered() mogą naruszyć deterministyczny porządek wyjścia.
GO TO FULL VERSION