1. Tạo stream
Để sử dụng Stream API, trước hết bạn cần lấy một stream từ một collection hoặc một mảng.
Ví dụ tạo stream
// Từ danh sách
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// Từ mảng
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// Từ các giá trị riêng lẻ
Stream<String> letters = Stream.of("A", "B", "C");
Tóm tắt:
- list.stream() — cho collection
- Arrays.stream(array) — cho mảng
- Stream.of(...) — cho các giá trị riêng lẻ
Ví dụ trong ngữ cảnh ứng dụng của chúng ta
Giả sử chúng ta có một danh sách người dùng:
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
Các thao tác trung gian và thao tác kết thúc
Điểm quan trọng: các thao tác trong Stream API được chia thành hai loại.
- Thao tác trung gian (ví dụ, filter, map, distinct) — mô tả các bước xử lý. Chúng trả về một stream mới, nhưng tự chúng không khởi chạy gì cả.
- Thao tác kết thúc (ví dụ, collect, forEach, count) — kích hoạt pipeline và trả ra kết quả.
Stream hoạt động “lười biếng”: cho tới khi gọi một thao tác kết thúc, sẽ không có phép tính nào diễn ra. Đó là lý do chúng ta thường kết thúc chuỗi bằng collect(...) — đây chính là điểm mà stream được biến lại thành collection hoặc một kết quả khác.
2. Thao tác filter: lọc phần tử theo điều kiện
filter là thao tác trung gian cho phép chỉ giữ lại các phần tử thỏa điều kiện đã cho.
Chữ ký
Stream<T> filter(Predicate<? super T> predicate);
Predicate là một functional interface nhận vào một phần tử và trả về true (giữ lại) hoặc false (loại bỏ).
Ví dụ: chỉ giữ các số chẵn
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
Điều gì đang diễn ra?
- n -> n % 2 == 0 — biểu thức lambda kiểm tra số có chia hết cho 2 hay không.
- filter giữ lại các số chẵn.
Ví dụ: lọc các tên bắt đầu bằng “A”
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
Lưu ý quan trọng: filter không thay đổi collection — nó tạo ra một stream mới chỉ chứa các phần tử cần thiết.
3. Thao tác map: biến đổi phần tử thành thứ khác
map là thao tác chuyển đổi. Nó lấy từng phần tử của stream, áp dụng một hàm và trả về phần tử mới.
Chữ ký
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function là một interface nhận vào một phần tử và trả về một giá trị (có thể là kiểu khác).
Ví dụ: lấy độ dài các chuỗi
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
Điều gì đang diễn ra?
- map biến chuỗi thành độ dài của nó (name -> name.length()).
- Kết quả thu được là một stream số.
Ví dụ: chuyển chuỗi sang chữ hoa
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. Thao tác collect: gom kết quả về collection
collect là thao tác kết thúc, tức là nó hoàn tất pipeline và gom kết quả vào collection hoặc một container khác.
Chữ ký
<R, A> R collect(Collector<? super T, A, R> collector)
Đừng sợ chữ ký dài dòng! Trong 99% trường hợp, bạn sẽ dùng các collector dựng sẵn trong lớp Collectors.
Collectors là một utility class với tập hợp các “bộ gom”. Nó cho stream biết cần gom kết quả về dạng nào: danh sách, tập hợp, chuỗi, v.v.
Ví dụ:
- Collectors.toList() — về List
- Collectors.toSet() — về Set
- Collectors.joining(", ") — thành một chuỗi, phân tách bằng dấu phẩy
Nói cách khác, Collectors giống như một bộ hộp với nhiều hình dạng khác nhau để bạn “đóng gói” các phần tử của stream.
Ví dụ: gom kết quả thành List
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Ví dụ: gom kết quả thành Set
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Ví dụ: ghép chuỗi bằng dấu phẩy
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. Chuỗi thao tác: lọc + biến đổi + gom kết quả
Điểm mạnh nhất của Stream API — là khả năng xâu chuỗi các thao tác liên tiếp.
Ví dụ: lấy độ dài các tên bắt đầu bằng “A”
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
Từng bước:
- .stream() — tạo stream từ danh sách.
- .filter(name -> name.startsWith("A")) — chỉ giữ các tên bắt đầu bằng "A".
- .map(String::length) — biến mỗi tên thành độ dài của nó.
- .collect(Collectors.toList()) — gom kết quả thành danh sách.
Mã kiểu mệnh lệnh tương đương
Đây là cách làm “cổ điển” sẽ trông như thế nào:
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
So sánh: Stream API — chỉ một dòng, đọc theo “làm gì” chứ không phải “làm như thế nào”.
6. Thực hành: một vài bài tập ngắn
Cùng luyện tập! Tất cả ví dụ có thể chạy trong một file — chỉ cần thay đổi dữ liệu.
Bài 1: Chỉ giữ các số lẻ và bình phương chúng
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
Bài 2: Từ danh sách chuỗi lấy danh sách chữ cái đầu tiên của chúng
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
Bài 3: Lọc các chuỗi có độ dài lớn hơn 3 và gom vào Set
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. Những lỗi thường gặp khi làm việc với filter, map, collect
Lỗi số 1: quên collect — không có kết quả!
Stream API lười, như mèo nằm bậu cửa: cho đến khi bạn gọi một thao tác kết thúc (ví dụ, collect hoặc forEach), sẽ không có gì xảy ra. Nếu chỉ viết users.stream().filter(...).map(...); — sẽ không thực thi hành động nào cả.
Lỗi số 2: đảo ngược thứ tự filter và map
Đôi khi người mới làm map trước rồi mới filter. Ví dụ, names.stream().map(String::length).filter(len -> len > 3) — sẽ cho ra các số, không phải chuỗi. Nếu bạn cần các chuỗi có độ dài lớn hơn 3, hãy lọc trước rồi mới chuyển đổi.
Lỗi số 3: quên tính bất biến
Các thao tác của Stream API không thay đổi collection gốc! Chúng trả về một kết quả mới. Sau List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — collection names vẫn giữ nguyên.
Lỗi số 4: cố gắng sử dụng một List bên ngoài có thể thay đổi
Không nên làm như sau:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
Tốt hơn hãy dùng collect — an toàn và ngắn gọn hơn.
Lỗi số 5: NullPointerException
Nếu trong collection có thể có phần tử null, thì gọi name.startsWith("A") trên null sẽ gây lỗi. Hãy thêm bộ lọc null nếu có thể:
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION