CodeGym /Các khóa học /JAVA 25 SELF /Phương thức join, concat: hợp nhất stream

Phương thức join, concat: hợp nhất stream

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Stream.concat: hợp nhất hai stream

Trong lập trình, thường gặp tình huống: chúng ta có hai (hoặc nhiều hơn) luồng dữ liệu và muốn hợp nhất thành một. Ví dụ, hai danh sách sinh viên từ các nhóm khác nhau — và cần xử lý tất cả cùng lúc. “Thời trước” (trước Stream API), ta chỉ việc gộp hai danh sách bằng addAll. Nhưng nếu làm việc với stream (Stream<T>), ta muốn thực hiện một cách lazy và biểu đạt hơn.

Cú pháp và nguyên tắc hoạt động

Cách cơ bản nhất để hợp nhất hai stream — dùng phương thức tĩnh Stream.concat:

Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)

Điều gì xảy ra:

  • Lấy stream thứ nhất trước, rồi đến stream thứ hai.
  • Kết quả là một stream mới, phát tất cả phần tử từ stream đầu tiên, rồi tất cả phần tử từ stream thứ hai.
  • Hợp nhất là lazy: cho đến khi bạn bắt đầu duyệt stream kết quả bằng một thao tác kết thúc (ví dụ, forEach hoặc collect), sẽ không có gì xảy ra.

Ví dụ: hợp nhất hai danh sách tên

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);

Kết quả:

Anya
Boris
Vika
Grisha
Dasha

Quan trọng: sau khi hợp nhất, stream kết quả chỉ dùng một lần. Giống như bất kỳ Stream nào khác, bạn không thể dùng lại nó.

Đặc điểm của Stream.concat

  • Chỉ hai stream mỗi lần. Với ba trở lên — hoặc xâu chuỗi concat, hoặc dùng cách khác (xem bên dưới).
  • Bảo toàn thứ tự. Trước là các phần tử của stream đầu tiên, sau đó là stream thứ hai.
  • Hợp nhất lazy. Nếu stream đầu tiên là vô hạn, sẽ không bao giờ tới stream thứ hai.
  • Lỗi thường gặp: cố gắng hợp nhất một stream với chính nó (Stream.concat(stream, stream)) dẫn đến việc tái sử dụng cùng một stream — điều này bị cấm.

2. Hợp nhất nhiều hơn hai stream: flatMap và Stream.of

Khi có nhiều hơn hai stream, thuận tiện hơn là gom chúng bằng kết hợp Stream.of + flatMap.

Dùng 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);

Điều gì diễn ra ở đây:

  • Stream.of(groupA, groupB, groupC) tạo ra một stream gồm ba collection.
  • flatMap(Collection::stream) trải mỗi collection thành một stream chuỗi duy nhất.
  • Kết quả — một stream lớn chứa tất cả sinh viên.

Ưu điểm: có thể hợp nhất bao nhiêu stream tùy thích.

Một cách khác: hợp nhất danh sách các stream

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);

Kết quả:

a
b
c
d
e

3. Collectors.joining: nối chuỗi với dấu phân cách

Đôi khi không chỉ hợp nhất stream, mà còn cần ghép mọi phần tử thành một chuỗi — ví dụ để in tên cách nhau bằng dấu phẩy. Khi đó dùng collector Collectors.joining.

Cú pháp

String result = stream.collect(Collectors.joining(", "));
  • Không đối số: ghép các chuỗi không có dấu phân cách.
  • Có dấu phân cách: giữa các phần tử sẽ là chuỗi đã chỉ định.
  • Có tiền tố và hậu tố: Collectors.joining(delimiter, prefix, suffix)

Ví dụ: danh sách sinh viên trong một chuỗi

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("Danh sách sinh viên: " + line);

Kết quả:

Danh sách sinh viên: Anya, Boris, Vika

Ví dụ với tiền tố và hậu tố

String line = students.stream()
    .collect(Collectors.joining(", ", "[", "]"));

System.out.println(line);

Kết quả:

[Anya, Boris, Vika]

Dùng để làm gì?

  • Tạo chuỗi CSV.
  • In danh sách và báo cáo đẹp mắt.
  • Truyền dữ liệu trong một chuỗi (ví dụ, vào URL hoặc log).

4. So sánh concat và flatMap: dùng khi nào?

  • Stream.concat — khi có đúng hai stream và cần giữ nguyên thứ tự.
  • Stream.of + flatMap — khi có nhiều stream hoặc chúng được lưu trong một collection.
  • Collectors.joining — khi phần tử là kiểu String và cần kết quả là một chuỗi.

Bảng so sánh

Phương thức Khi dùng Ví dụ mã
Stream.concat(a, b)
Hai stream
Stream.concat(a, b)
Stream.of(...).flatMap()
Nhiều stream, collection các stream
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
Nối các phần tử thành một chuỗi
stream.collect(Collectors.joining(", "))

5. Ví dụ thực hành trong ứng dụng học tập

Ví dụ 1. Hợp nhất sinh viên từ hai khoa

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);

Ví dụ 2. Hợp nhất sinh viên từ mọi nhóm, in thành một chuỗi

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("Tất cả sinh viên: " + allNames);

Kết quả:

Tất cả sinh viên: Anya; Boris; Vika; Grisha; Dasha

Ví dụ 3. Hợp nhất các stream số

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. Những lưu ý quan trọng và đặc điểm

concat chỉ hoạt động với hai stream

Với ba stream sẽ thành biểu thức lồng nhau — vẫn hoạt động, nhưng khá rối. Tốt hơn nên dùng 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);

Stream chỉ dùng một lần

Sau một thao tác kết thúc, stream không thể dùng lại. Tái sử dụng sẽ dẫn đến IllegalStateException.

Stream concat là lazy

Nếu stream đầu tiên là vô hạn (ví dụ, tạo qua Stream.generate(...)), sẽ không bao giờ tới stream thứ hai.

Thứ tự phần tử

Thứ tự luôn được giữ nguyên: trước stream đầu tiên, sau đó stream thứ hai.

Collectors.joining chỉ làm việc với Stream<String>

Với non-String, hãy chuyển phần tử sang chuỗi trước, ví dụ qua 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

So sánh với “cách thủ công”

Trước Stream API:

import java.util.ArrayList;
import java.util.List;

List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);

Với Stream API:

import java.util.stream.Collectors;

List<String> merged = Stream.concat(listA.stream(), listB.stream())
    .collect(Collectors.toList());

Ưu điểm của stream: có thể chèn các thao tác trung gian (filter, map, v.v.) trước và sau khi hợp nhất, cũng như làm việc với mọi nguồn dữ liệu.

7. Lỗi thường gặp khi hợp nhất stream

Lỗi số 1: tái sử dụng stream. Stream chỉ có thể dùng một lần. Không thể hợp nhất một stream với chính nó.

import java.util.stream.Stream;

Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Lỗi! s sẽ bị dùng lại

Lỗi số 2: cố gắng hợp nhất một stream vô hạn với một stream hữu hạn. Nếu stream đầu tiên là vô hạn, stream thứ hai sẽ không bao giờ bắt đầu.

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 sẽ không vào kết quả

Lỗi số 3: joining cho non-String. Collector Collectors.joining nhận một stream chuỗi. Với số, cần chuyển sang chuỗi trước:

import java.util.List;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Lỗi biên dịch!

String joined = numbers.stream()
    .map(Object::toString)
    .collect(Collectors.joining(", "));

Lỗi số 4: vi phạm thứ tự. Nếu thứ tự là quan trọng, hãy dùng concat hoặc flatMap có thứ tự. Những phương thức như unordered() có thể phá vỡ thứ tự xác định của đầu ra.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Thu hoạch cho món salad trái cây hoành tráng 🍎🍊
Thu hoạch cho món salad trái cây hoành tráng 🍎🍊
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Thiết kế cửa sổ trưng bày các thiên thể 🚀
Thiết kế cửa sổ trưng bày các thiên thể 🚀
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION