CodeGym /Kursy /JAVA 25 SELF /Metody join, concat: łączenie strumieni

Metody join, concat: łączenie strumieni

JAVA 25 SELF
Poziom 32, Lekcja 1
Dostępny

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
Stream.concat(a, b)
Dwa strumienie
Stream.concat(a, b)
Stream.of(...).flatMap()
Wiele strumieni, kolekcja strumieni
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
Skleić elementy w jeden łańcuch
stream.collect(Collectors.joining(", "))

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.

1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 1
Niedostępne
Zbiór na wspaniałą sałatkę owocową 🍎🍊
Zbiór na wspaniałą sałatkę owocową 🍎🍊
1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 1
Niedostępne
Aranżacja witryny z obiektami kosmicznymi 🚀
Aranżacja witryny z obiektami kosmicznymi 🚀
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION