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ã |
|---|---|---|
|
Hai stream | |
|
Nhiều stream, collection các stream | |
|
Nối các phần tử thành một chuỗi | |
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.
GO TO FULL VERSION