Buổi 10: Java hiện đại: Lambda, Stream và tổng kết khóa
Hãy tưởng tượng bạn là lập trình viên của một cửa hàng sách trực tuyến. Mỗi sáng, quản lý yêu cầu: "Lấy cho tôi danh sách các cuốn sách còn hàng, giá trên 100 nghìn, sắp xếp theo giá giảm dần, rồi in ra tên của năm cuốn đầu tiên". Với những gì đã học, bạn sẽ viết một vòng for kèm if, một danh sách tạm, một Comparator viết bằng lớp ẩn danh, rồi một vòng for nữa để in. Làm đúng thì đúng, nhưng ba chục dòng mã chỉ để diễn đạt một câu yêu cầu ngắn gọn.
Từ Java 8, ngôn ngữ cho bạn một cách viết khác: mô tả cái bạn muốn thay vì mô tả từng bước làm. Bạn truyền "một đoạn hành vi" (biểu thức lambda) vào các phương thức, và nối các phép toán thành một dây chuyền Stream — lọc, biến đổi, sắp xếp, gom nhóm — chỉ trong vài dòng đọc gần như tiếng Anh. Đây là buổi cuối của khóa: bạn học hết phần "Java hiện đại" của đề cương, rồi nhìn lại toàn bộ hành trình mười buổi và định hướng cho dự án nhóm cuối kỳ cùng lộ trình học tiếp.
Mục tiêu buổi học
- Sau buổi này bạn có thể giải thích giao diện hàm (functional interface) và viết biểu thức lambda thay cho lớp ẩn danh.
- Sau buổi này bạn có thể chọn đúng giao diện hàm chuẩn (
Function,Predicate,Supplier,Consumer,UnaryOperator,BinaryOperator) và dùng tham chiếu phương thức (String::length,System.out::println). - Sau buổi này bạn có thể dùng
Optionalđể biểu diễn "có thể không có giá trị" mà không phải kiểm tranullthủ công. - Sau buổi này bạn có thể xây dựng pipeline Stream: tạo luồng,
filter/map/flatMap/sorted/distinct/limit, rồi kết thúc bằngreduce,collecthoặc các phép thống kê. - Sau buổi này bạn có thể gom nhóm dữ liệu bằng
Collectors.groupingBy,partitioningBy,joining,counting,averagingDoublevà giải thích khi nào nên (không nên) dùng luồng song song. - Sau buổi này bạn có thể tóm tắt toàn bộ khóa học, lập kế hoạch dự án nhóm cuối kỳ và vạch ra lộ trình học Java tiếp theo.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1 | Giao diện hàm, biểu thức lambda, tham chiếu phương thức | 35 phút (gồm thực hành) |
| 2 | Optional |
15 phút |
| 3 | Stream API: tạo luồng, phép trung gian, phép kết thúc | 35 phút (gồm thực hành) |
| 4 | sorted, limit, reduce và thống kê số học |
25 phút (gồm thực hành) |
| 5 | collect: joining, groupingBy, partitioningBy, toMap |
35 phút (gồm thực hành) |
| 6 | Luồng song song sơ lược | 5 phút |
| 7 | Bài toán tổng hợp bằng Stream | 15 phút (thực hành) |
| 8 | Tổng kết khóa: bản đồ 10 buổi, dự án nhóm, ôn tập, lộ trình | 15 phút |
Khoảng 55% thời gian là thực hành: bạn gõ lại và sửa các ví dụ có nút Chạy thử, rồi làm các nhóm bài tập chấm tự động sau mỗi phần.
1. Giao diện hàm và biểu thức Lambda
1.1. Từ lớp ẩn danh đến lambda
Ở buổi 7 bạn đã sắp xếp danh sách bằng Comparator. Cách "truyền thống" là viết một lớp ẩn danh (anonymous class): vừa khai báo lớp, vừa tạo đối tượng, chỉ để chứa một phương thức. Phần lớn ký tự trong đoạn mã dưới đây là nghi thức, không phải logic.
List<String> names = new ArrayList<>(List.of("Lan", "An", "Minh", "Hoa"));
// Cách cũ: lớp ẩn danh
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println(names);Cùng ý đó, viết bằng lambda chỉ còn một dòng: "nhận hai chuỗi a, b, trả về hiệu độ dài".
List<String> names = new ArrayList<>(List.of("Lan", "An", "Minh", "Hoa"));
names.sort((a, b) -> a.length() - b.length());
System.out.println(names);Hai kết quả giống nhau, vì sort là sắp xếp ổn định (stable): các chuỗi cùng độ dài giữ nguyên thứ tự ban đầu.
Lưu ý: Lambda không phải "tính năng mới hoàn toàn" mà là cách viết gọn cho đối tượng của một giao diện chỉ có một phương thức trừu tượng. Trình biên dịch tự suy ra kiểu của
a,btừ ngữ cảnh.
1.2. Giao diện hàm (functional interface)
Giao diện hàm là interface có đúng một phương thức trừu tượng (có thể kèm nhiều phương thức default/static). Nơi nào cần một đối tượng thuộc giao diện hàm, bạn được phép đưa vào một lambda. Chú thích @FunctionalInterface không bắt buộc, nhưng khiến trình biên dịch báo lỗi nếu bạn lỡ thêm phương thức trừu tượng thứ hai.
@FunctionalInterface
interface Calculator {
int apply(int a, int b);
}
public class Main {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
Calculator mul = (a, b) -> a * b;
System.out.println(add.apply(6, 7));
System.out.println(mul.apply(6, 7));
}
}Bạn vừa định nghĩa "hành vi" như một giá trị: add và mul là hai biến thuộc cùng kiểu, nhưng làm hai việc khác nhau. Đây chính là tinh thần lập trình hàm (functional programming) mà Java vay mượn: hàm là dữ liệu, có thể gán vào biến, truyền vào phương thức, trả về từ phương thức.
Ví dụ thực tế: Một ứng dụng ngân hàng có nhiều cách tính phí chuyển khoản (miễn phí, theo phần trăm, theo bậc thang). Thay vì viết ba lớp, bạn khai báo một giao diện hàm
FeeRulevà truyền vào ba lambda khác nhau tùy loại giao dịch.
1.3. Cú pháp biểu thức lambda
Lambda có dạng (danh sách tham số) -> thân hàm. Các biến thể thường gặp:
| Dạng | Ví dụ | Khi dùng |
|---|---|---|
| Một biểu thức, có kiểu | (int a, int b) -> a + b |
Muốn ghi rõ kiểu |
| Một biểu thức, suy kiểu | (a, b) -> a + b |
Dạng phổ biến nhất |
| Một tham số | x -> x * x |
Có thể bỏ ngoặc tròn |
| Không tham số | () -> 42 |
Cho Supplier, Runnable |
| Thân là khối lệnh | (a, b) -> { int s = a + b; return s; } |
Cần nhiều lệnh, phải có return |
Function<Integer, Integer> square = x -> x * x;
BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
Supplier<String> hello = () -> "Xin chào Lambda";
BiFunction<Integer, Integer, Integer> sumSquares = (a, b) -> {
int sa = a * a;
int sb = b * b;
return sa + sb;
};
System.out.println(square.apply(9));
System.out.println(max.apply(3, 8));
System.out.println(hello.get());
System.out.println(sumSquares.apply(3, 4));1.4. Biến bắt giữ và effectively final
Thân lambda được phép dùng biến cục bộ của phương thức bao quanh, nhưng biến đó phải là final hoặc effectively final (được gán đúng một lần và không bao giờ thay đổi). Lý do: lambda có thể chạy muộn hơn, thậm chí ở luồng khác, nên Java chỉ cho phép "chụp" giá trị của biến chứ không cho phép sửa biến gốc.
int bonus = 10; // gán một lần: effectively final
Function<Integer, Integer> addBonus = score -> score + bonus;
System.out.println(addBonus.apply(75));
// Muốn "tích lũy" trong lambda? Dùng đối tượng thay đổi được thay vì biến cục bộ
int[] counter = {0};
List.of("a", "b", "c").forEach(s -> counter[0]++);
System.out.println(counter[0]);Cảnh báo: Nếu bạn viết
int total = 0; list.forEach(x -> total += x);trình biên dịch báo lỗi "local variables referenced from a lambda expression must be final or effectively final". Mẹo mảng một phần tử ở trên chạy được nhưng không phải phong cách tốt — ở các phần sau bạn sẽ thay bằngreduce/sumcủa Stream.
1.5. Các giao diện hàm chuẩn trong java.util.function
Bạn hiếm khi phải tự khai báo giao diện hàm: JDK đã có sẵn bộ giao diện hàm chuẩn trong gói java.util.function. Sáu loại cần nhớ:
| Giao diện | Phương thức | Ý nghĩa | Ví dụ |
|---|---|---|---|
Function<T, R> |
R apply(T t) |
Biến đổi T thành R |
s -> s.length() |
Predicate<T> |
boolean test(T t) |
Kiểm tra điều kiện | n -> n % 2 == 0 |
Supplier<T> |
T get() |
Cung cấp giá trị, không nhận gì | () -> "abc" |
Consumer<T> |
void accept(T t) |
Tiêu thụ giá trị, không trả về | s -> System.out.println(s) |
UnaryOperator<T> |
T apply(T t) |
Function có kiểu vào = kiểu ra |
s -> s.trim() |
BinaryOperator<T> |
T apply(T a, T b) |
Hai T vào, một T ra |
(a, b) -> a + b |
Ngoài ra còn biến thể cho số nguyên thủy để tránh đóng/mở hộp (boxing): IntPredicate, IntFunction, IntBinaryOperator, LongBinaryOperator, ToIntFunction... và biến thể hai tham số như BiFunction, BiConsumer, BiPredicate. Quy tắc nhớ: tên cho biết "vào gì, ra gì".
Ví dụ tiếp theo dùng bốn giao diện đầu trong một tình huống điểm sinh viên.
Predicate<Double> passed = score -> score >= 5.0;
Function<Double, String> rank = score -> score >= 8 ? "Giỏi" : score >= 5 ? "Đạt" : "Trượt";
Consumer<String> printer = s -> System.out.println("Kết quả: " + s);
Supplier<Double> defaultScore = () -> 0.0;
double s1 = 8.5;
double s2 = 4.0;
System.out.println(passed.test(s1) + " " + passed.test(s2));
printer.accept(rank.apply(s1));
printer.accept(rank.apply(s2));
System.out.println(defaultScore.get());UnaryOperator và BinaryOperator là hai "họ hàng" của Function: khi kiểu vào và kiểu ra giống nhau, dùng chúng cho gọn và đọc là hiểu ngay ý định.
UnaryOperator<String> shout = s -> s.toUpperCase() + "!";
BinaryOperator<Integer> add = (a, b) -> a + b;
LongBinaryOperator power = (base, exp) -> {
long r = 1;
for (long i = 0; i < exp; i++) r *= base;
return r;
};
System.out.println(shout.apply("java"));
System.out.println(add.apply(20, 22));
System.out.println(power.applyAsLong(2, 20));1.6. Kết hợp giao diện hàm: andThen, compose, and, or, negate
Giao diện hàm chuẩn có sẵn các phương thức default để ghép hành vi nhỏ thành hành vi lớn mà không phải viết lại lambda.
Function<Integer, Integer> plusOne = x -> x + 1;
Function<Integer, Integer> doubleIt = x -> x * 2;
System.out.println(plusOne.andThen(doubleIt).apply(5)); // (5+1)*2
System.out.println(plusOne.compose(doubleIt).apply(5)); // (5*2)+1
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> big = n -> n > 50;
System.out.println(even.and(big).test(60));
System.out.println(even.and(big).test(40));
System.out.println(even.or(big).test(51));
System.out.println(even.negate().test(7));f.andThen(g) chạy f rồi đến g; f.compose(g) chạy g rồi đến f. Hãy để ý thứ tự khi đọc kết quả 12 và 11 ở trên.
1.7. Tham chiếu phương thức (method reference)
Khi lambda chỉ làm đúng một việc là gọi một phương thức có sẵn, bạn viết gọn hơn bằng toán tử ::.
| Dạng | Lambda tương đương | Ví dụ |
|---|---|---|
| Phương thức tĩnh | x -> Integer.parseInt(x) |
Integer::parseInt |
| Phương thức của đối tượng cụ thể | x -> System.out.println(x) |
System.out::println |
| Phương thức của đối tượng tùy ý (kiểu cho trước) | s -> s.length() |
String::length |
| Hàm tạo | () -> new ArrayList<>() |
ArrayList::new |
List<String> raw = List.of("12", "7", "30");
Function<String, Integer> parse = Integer::parseInt;
Function<String, Integer> len = String::length;
Supplier<List<String>> maker = ArrayList::new;
raw.forEach(System.out::println);
System.out.println(parse.apply("2026") + 1);
System.out.println(len.apply("lambda"));
List<String> fresh = maker.get();
fresh.add("mới");
System.out.println(fresh);Mẹo: Đừng ép dùng
::bằng mọi giá. Nếu tham chiếu phương thức làm dòng mã khó đọc hơn lambda, hãy giữ lambda. Mục tiêu là dễ đọc, không phải ngắn nhất.
Vừa học xong giao diện hàm và lambda, hãy làm bốn bài sau để tự viết Predicate, Function, Consumer và một giao diện hàm riêng cho các phép tính; lưu ý chọn đúng giao diện theo "vào gì, ra gì".
2. Optional — "có thể không có giá trị"
2.1. Vấn đề của null
Một phương thức tìm sinh viên theo mã có thể không tìm thấy ai. Trả về null là cách quen thuộc, nhưng người gọi rất dễ quên kiểm tra và gặp NullPointerException ở một chỗ xa nơi gây ra lỗi. Optional<T> là một "chiếc hộp" chứa hoặc không chứa đúng một giá trị kiểu T. Kiểu trả về Optional<Student> nói thẳng với người gọi: "kết quả có thể vắng, hãy xử lý cả hai trường hợp".
2.2. Tạo và đọc Optional
| Cách tạo | Ý nghĩa |
|---|---|
Optional.of(x) |
Hộp có giá trị; x là null thì ném NullPointerException |
Optional.ofNullable(x) |
Có giá trị nếu x != null, ngược lại hộp rỗng |
Optional.empty() |
Hộp rỗng |
| Cách đọc | Ý nghĩa |
|---|---|
isPresent() / isEmpty() (Java 11) |
Có / không có giá trị |
get() |
Lấy giá trị; ném lỗi nếu rỗng — tránh dùng trần |
orElse(v) |
Có thì lấy, rỗng thì dùng v |
orElseGet(supplier) |
Như orElse nhưng chỉ tính khi rỗng |
orElseThrow(supplier) |
Rỗng thì ném ngoại lệ do bạn chọn |
ifPresent(consumer) |
Có thì chạy hành động |
map(fn) / filter(pred) |
Biến đổi / lọc giá trị bên trong hộp |
public class Main {
static Optional<String> findName(Map<Integer, String> db, int id) {
return Optional.ofNullable(db.get(id));
}
public static void main(String[] args) {
Map<Integer, String> db = new TreeMap<>();
db.put(1, "An");
db.put(2, "Bình");
System.out.println(findName(db, 1).orElse("không rõ"));
System.out.println(findName(db, 9).orElse("không rõ"));
findName(db, 2).ifPresent(n -> System.out.println("Tìm thấy: " + n));
System.out.println(findName(db, 2).map(String::toUpperCase).get());
System.out.println(findName(db, 9).isPresent());
}
}Phương thức findName không bao giờ trả null: kết quả vắng mặt được nói rõ bằng hộp rỗng, và người gọi buộc phải chọn cách xử lý.
Cảnh báo: Không viết
opt.get()mà không kiểm tra trước — rỗng sẽ némNoSuchElementException, thànhNullPointerExceptiontrá hình. Hãy ưu tiênorElse,orElseGet,orElseThrow,ifPresent.
2.3. Optional và phong cách "chuỗi gọi"
Vì map và filter trả về Optional, bạn có thể xâu chuỗi nhiều bước mà không cần if: nếu một bước làm hộp rỗng, các bước sau tự bỏ qua.
Optional<String> input = Optional.of(" Java 13 ");
String result = input
.map(String::trim)
.filter(s -> s.length() > 3)
.map(s -> s.toUpperCase())
.orElse("RỖNG");
System.out.println(result);
Optional<String> blank = Optional.of(" ");
System.out.println(blank.map(String::trim).filter(s -> !s.isEmpty()).orElse("RỖNG"));Mẹo: Dùng
Optionallàm kiểu trả về của phương thức. Đừng dùng nó cho thuộc tính của lớp hay tham số phương thức — đó không phải mục đích thiết kế của nó.
3. Stream API: tạo luồng, phép trung gian, phép kết thúc
3.1. Stream là gì?
Stream là một dây chuyền xử lý dữ liệu: dữ liệu "chảy" từ một nguồn (danh sách, mảng, dãy số...) qua các phép trung gian (lọc, biến đổi, sắp xếp...) rồi tới một phép kết thúc (tính tổng, gom thành danh sách, in ra...). Điểm khác biệt quan trọng so với Collection:
| Tiêu chí | Collection | Stream |
|---|---|---|
| Vai trò | Lưu trữ dữ liệu | Xử lý dữ liệu |
| Sửa nguồn | Có (add, remove) |
Không — không đụng tới nguồn |
| Thực thi | Chạy ngay | Lười (lazy): chỉ chạy khi gặp phép kết thúc |
| Dùng lại | Dùng được nhiều lần | Dùng một lần — gọi lại sẽ ném IllegalStateException |
list.stream().filter(x -> x > 3).map(x -> x * 10).forEach(System.out::println);
Kết quả in ra: 50 80 60 90 (chỉ các phần tử > 3, đã nhân 10).
list.stream().filter(x -> x > 3).map(x -> x * 10);Không có phép kết thúc ⇒ không chạy gì cả.Phép trung gian (filter, map, sorted…) chỉ mô tả; phép kết thúc mới kéo dữ liệu. Mỗi phần tử đi hết pipeline rồi tới phần tử kế tiếp (trừ sorted(): phải gom đủ trước).
Hình trên vẽ ba giai đoạn của pipeline: nguồn, các phép trung gian nối tiếp nhau, và phép kết thúc. Hãy để ý rằng chỉ khi có phép kết thúc thì dữ liệu mới thật sự chảy.
Lưu ý: Stream không liên quan gì đến
InputStream/OutputStreamcủa buổi 8. Cùng chữ "stream" nhưng đó là hai khái niệm khác nhau: một bên là dòng byte/ký tự vào ra, một bên là dây chuyền xử lý dữ liệu trong bộ nhớ.
3.2. Tạo Stream
| Nguồn | Cách tạo |
|---|---|
| Collection | list.stream() |
| Mảng | Arrays.stream(arr) |
| Vài giá trị rời | Stream.of("a", "b", "c") |
| Dãy số nguyên | IntStream.range(0, 5), IntStream.rangeClosed(1, 5) |
| Vô hạn | Stream.iterate(seed, f), Stream.generate(supplier) (phải kèm limit) |
List<String> list = List.of("an", "binh", "chi");
int[] arr = {4, 8, 15, 16, 23, 42};
System.out.println(list.stream().count());
System.out.println(Arrays.stream(arr).sum());
System.out.println(Stream.of(3, 1, 2).count());
System.out.println(IntStream.rangeClosed(1, 5).sum());
System.out.println(Stream.iterate(1, x -> x * 2).limit(8).collect(Collectors.toList()));Với số nguyên thủy, Java có ba "luồng chuyên biệt" IntStream, LongStream, DoubleStream: nhanh hơn (không đóng hộp) và có sẵn sum(), average(), max(), summaryStatistics(). Chuyển qua lại bằng mapToInt, mapToObj, boxed().
3.3. Phép trung gian: filter, map, flatMap
Phép trung gian luôn trả về một Stream mới và chưa chạy gì cả.
| Phép | Việc làm | Tương tự SQL |
|---|---|---|
filter(Predicate) |
Giữ phần tử thỏa điều kiện | WHERE |
map(Function) |
Biến đổi từng phần tử | SELECT biểu_thức |
flatMap(Function) |
Biến mỗi phần tử thành một luồng rồi "dàn phẳng" | — |
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> result = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(result);
List<String> words = List.of("java", "lambda", "stream");
System.out.println(words.stream().map(String::length).collect(Collectors.toList()));
System.out.println(nums);Dòng cuối chứng minh Stream không sửa nguồn: nums vẫn nguyên vẹn.
flatMap hữu ích khi mỗi phần tử đã là một "tập con". Ví dụ mỗi đơn hàng chứa nhiều mặt hàng và bạn muốn danh sách phẳng tất cả mặt hàng.
List<List<String>> orders = List.of(
List.of("bút", "vở"),
List.of("thước"),
List.of("bút", "tẩy", "cặp"));
List<String> allItems = orders.stream()
.flatMap(List::stream)
.distinct()
.collect(Collectors.toList());
System.out.println(allItems);
String sentence = "xin chao java";
System.out.println(Arrays.stream(sentence.split(" ")).map(String::toUpperCase).collect(Collectors.toList()));3.4. Tính lười và thứ tự thực thi
Stream xử lý từng phần tử một qua cả dây chuyền (không phải xong bước 1 cho cả danh sách rồi mới sang bước 2). Phép peek giúp "nhìn trộm" dữ liệu chảy qua, rất hợp để quan sát; chỉ dùng khi học và gỡ lỗi.
List<Integer> data = List.of(1, 2, 3, 4);
Stream<Integer> pipeline = data.stream()
.peek(x -> System.out.println("vào " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("giữ " + x));
System.out.println("chưa có gì chạy...");
long count = pipeline.count();
System.out.println("đếm được " + count);Hãy đọc đầu ra: dòng "chưa có gì chạy..." xuất hiện trước mọi dòng "vào", và các dòng "vào/giữ" đan xen theo từng phần tử. Đó là tính lười + xử lý theo phần tử.
Dùng thử mô phỏng dưới đây để ghép chuỗi phép toán rồi quan sát dữ liệu chảy qua từng bước.
Phòng thí nghiệm Stream
Ghép các phép, xem từng phần tử chảy qua pipeline (lười, từng cái một)List<Integer> kq = List.of(5, 2, 8, 3, 6, 1, 9)
.stream()
.filter(x -> x > 3)
.map(x -> x * 10)
.collect(Collectors.toList());Bạn hãy thử: đặt filter trước map, rồi đảo ngược lại; xem số phần tử đi qua mỗi bước thay đổi thế nào.
3.5. Phép kết thúc cơ bản
Phép kết thúc kích hoạt pipeline và trả về kết quả (hoặc không trả gì).
| Phép | Kết quả | Ghi chú |
|---|---|---|
forEach(Consumer) |
void |
In/ghi từng phần tử |
count() |
long |
Đếm |
collect(Collector) |
tùy bộ gom | Gom thành List/Set/Map/chuỗi... |
toArray() |
mảng | toArray(String[]::new) |
findFirst() / findAny() |
Optional<T> |
Phần tử đầu / bất kỳ |
anyMatch / allMatch / noneMatch |
boolean |
Có/mọi/không phần tử nào thỏa |
min(cmp) / max(cmp) |
Optional<T> |
Nhỏ nhất / lớn nhất |
reduce(...) |
giá trị / Optional |
Gộp dần (phần 4) |
List<Integer> scores = List.of(45, 78, 92, 60, 33, 81);
System.out.println(scores.stream().filter(s -> s >= 50).count());
System.out.println(scores.stream().anyMatch(s -> s > 90));
System.out.println(scores.stream().allMatch(s -> s >= 50));
System.out.println(scores.stream().noneMatch(s -> s < 0));
System.out.println(scores.stream().filter(s -> s > 80).findFirst().get());
System.out.println(scores.stream().max(Integer::compare).get());
Optional<Integer> none = scores.stream().filter(s -> s > 100).findFirst();
System.out.println(none.isPresent());Cảnh báo: Một Stream chỉ dùng được một lần. Gọi thêm phép kết thúc trên cùng một biến Stream đã dùng sẽ ném
IllegalStateException: stream has already been operated upon or closed. Cần dùng lại thì hãy tạo Stream mới từ nguồn.
Vừa học xong cách tạo Stream, map/filter và Optional, hãy làm các bài sau để quen với việc đổi vòng for thành pipeline: lúc đầu bạn thấy lạ, nhưng làm đủ bốn bài sẽ thành phản xạ.
4. sorted, distinct, limit, reduce và thống kê
4.1. sorted, distinct, limit, skip
| Phép | Việc làm | Lưu ý |
|---|---|---|
sorted() |
Sắp xếp theo thứ tự tự nhiên | Phần tử phải Comparable |
sorted(Comparator) |
Sắp xếp theo tiêu chí riêng | Comparator.reverseOrder(), Comparator.comparing(...) |
distinct() |
Bỏ phần tử trùng (dùng equals) |
Giữ thứ tự xuất hiện đầu tiên |
limit(n) |
Chỉ lấy n phần tử đầu |
Cắt ngắn — rất cần cho luồng vô hạn |
skip(n) |
Bỏ n phần tử đầu |
Dùng để phân trang |
List<Integer> nums = List.of(5, 3, 8, 3, 9, 1, 8, 7);
System.out.println(nums.stream().sorted().collect(Collectors.toList()));
System.out.println(nums.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList()));
System.out.println(nums.stream().distinct().collect(Collectors.toList()));
// Top 3 số lớn nhất, không trùng
System.out.println(nums.stream().distinct().sorted(Comparator.reverseOrder()).limit(3).collect(Collectors.toList()));
// Trang thứ hai, mỗi trang 3 phần tử
System.out.println(nums.stream().skip(3).limit(3).collect(Collectors.toList()));Với đối tượng, hãy dùng Comparator.comparing kèm tham chiếu phương thức — gọn và rõ nghĩa hơn nhiều so với lớp ẩn danh ở đầu buổi.
class Book {
String title;
double price;
Book(String title, double price) { this.title = title; this.price = price; }
String getTitle() { return title; }
double getPrice() { return price; }
}
public class Main {
public static void main(String[] args) {
List<Book> books = List.of(
new Book("Java căn bản", 120_000),
new Book("Cấu trúc dữ liệu", 150_000),
new Book("Cơ sở dữ liệu", 95_000),
new Book("Mạng máy tính", 150_000));
books.stream()
.sorted(Comparator.comparing(Book::getPrice).reversed().thenComparing(Book::getTitle))
.limit(3)
.forEach(b -> System.out.println(b.getTitle() + " - " + (long) b.getPrice()));
}
}thenComparing phá thế hòa: hai cuốn cùng giá 150.000 được xếp tiếp theo tên.
4.2. reduce: gộp cả luồng thành một giá trị
reduce lấy phần tử đầu làm giá trị tích lũy rồi lần lượt "gộp" từng phần tử tiếp theo vào bằng một BinaryOperator. Có hai dạng:
| Dạng | Kết quả | Khi luồng rỗng |
|---|---|---|
reduce(identity, op) |
giá trị cùng kiểu | Trả về identity |
reduce(op) |
Optional<T> |
Trả về Optional.empty() |
identity là "phần tử trung hòa" của phép gộp: 0 cho phép cộng, 1 cho phép nhân, chuỗi rỗng cho nối chuỗi.
List<Integer> nums = List.of(2, 3, 4, 5);
int sum = nums.stream().reduce(0, Integer::sum);
int product = nums.stream().reduce(1, (a, b) -> a * b);
Optional<Integer> max = nums.stream().reduce(Integer::max);
String joined = Stream.of("a", "b", "c").reduce("", (acc, s) -> acc + s);
System.out.println(sum);
System.out.println(product);
System.out.println(max.get());
System.out.println(joined);
System.out.println(new ArrayList<Integer>().stream().reduce(0, Integer::sum));
System.out.println(new ArrayList<Integer>().stream().reduce(Integer::sum).isPresent());Lưu ý: Phép gộp của
reducephải có tính kết hợp (associative), ví dụ cộng, nhân,max. Phép trừ thì không: kết quả sẽ khác nhau khi chạy song song.
4.3. Thống kê số học với IntStream
Với dữ liệu số, hãy chuyển sang luồng chuyên biệt bằng mapToInt/mapToDouble rồi dùng các phép có sẵn thay vì tự reduce.
List<Integer> scores = List.of(72, 85, 90, 64, 78);
System.out.println(scores.stream().mapToInt(Integer::intValue).sum());
System.out.println(scores.stream().mapToInt(Integer::intValue).max().getAsInt());
System.out.println(scores.stream().mapToInt(Integer::intValue).average().orElse(0));
IntSummaryStatistics st = scores.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println(st.getCount() + " " + st.getMin() + " " + st.getMax() + " " + st.getSum());
System.out.printf("%.2f%n", st.getAverage());average() trả về OptionalDouble (luồng rỗng thì không có trung bình), nên cần orElse/getAsDouble. summaryStatistics() tính cả đếm, nhỏ nhất, lớn nhất, tổng, trung bình chỉ trong một lần duyệt.
Mẹo: Đề bài thường dặn "nếu danh sách rỗng thì in 0". Đó chính là lúc
identitycủareducehoặcorElse(0)củaaverage()phát huy tác dụng.
Vừa học xong sorted, limit, reduce và thống kê, hãy làm bốn bài sau: sắp xếp giảm dần, tổng bằng reduce, lấy top K phần tử lớn nhất và in thống kê tổng hợp; chú ý định dạng số thập phân của đề.
5. collect: gom kết quả với Collectors
5.1. Gom thành List, Set, chuỗi
Phép collect nhận một Collector — "công thức gom" dựng sẵn trong lớp Collectors.
| Collector | Kết quả |
|---|---|
toList() / toSet() |
List / Set |
toCollection(TreeSet::new) |
Kiểu tập hợp tự chọn |
joining(", ", "[", "]") |
Một String: phân cách, tiền tố, hậu tố |
counting() |
Long — số phần tử |
summingInt(f), averagingDouble(f) |
Tổng / trung bình của một thuộc tính |
toMap(keyFn, valueFn) |
Map (khóa trùng sẽ ném lỗi, trừ khi có hàm trộn) |
List<String> names = List.of("lan", "an", "minh", "an", "hoa");
System.out.println(names.stream().distinct().collect(Collectors.toList()));
TreeSet<String> sorted = names.stream().collect(Collectors.toCollection(TreeSet::new));
System.out.println(sorted);
System.out.println(names.stream().map(String::toUpperCase).collect(Collectors.joining(", ", "[", "]")));
System.out.println(names.stream().collect(Collectors.counting()));
Map<String, Integer> lengths = names.stream().distinct()
.collect(Collectors.toMap(n -> n, String::length, (a, b) -> a, TreeMap::new));
System.out.println(lengths);Tham số thứ ba của toMap là hàm trộn khi gặp khóa trùng; tham số thứ tư chọn loại Map (ở đây TreeMap để thứ tự in ra ổn định).
5.2. groupingBy: gom nhóm như GROUP BY của SQL
Collectors.groupingBy(hàm_phân_loại) trả về Map<Khóa, List<Phần tử>>. Thêm một collector hạ nguồn (downstream) để quyết định "làm gì với mỗi nhóm": đếm, cộng, lấy trung bình, ánh xạ...
class Student {
String name; String clazz; double gpa;
Student(String name, String clazz, double gpa) { this.name = name; this.clazz = clazz; this.gpa = gpa; }
String getName() { return name; }
String getClazz() { return clazz; }
double getGpa() { return gpa; }
}
public class Main {
public static void main(String[] args) {
List<Student> list = List.of(
new Student("An", "KTPM1", 3.2), new Student("Bình", "KHMT1", 3.6),
new Student("Chi", "KTPM1", 2.8), new Student("Dũng", "KHMT1", 3.0),
new Student("Em", "KTPM1", 3.5));
Map<String, List<String>> names = list.stream().collect(Collectors.groupingBy(
Student::getClazz, TreeMap::new, Collectors.mapping(Student::getName, Collectors.toList())));
Map<String, Long> counts = list.stream().collect(Collectors.groupingBy(
Student::getClazz, TreeMap::new, Collectors.counting()));
Map<String, Double> avg = list.stream().collect(Collectors.groupingBy(
Student::getClazz, TreeMap::new, Collectors.averagingDouble(Student::getGpa)));
System.out.println(names);
System.out.println(counts);
avg.forEach((k, v) -> System.out.printf("%s: %.2f%n", k, v));
}
}Dạng ba tham số groupingBy(phân loại, mapFactory, downstream) cho phép chọn TreeMap để khóa luôn được sắp xếp — rất cần khi đề bài yêu cầu in theo thứ tự khóa tăng dần.
Gom nhóm hai tầng cũng chỉ là lồng collector này vào collector kia, ví dụ nhóm theo lớp rồi theo mức xếp loại. Bài tập cuối buổi có đúng dạng này.
Lưu ý:
groupingBymặc định dùngHashMap, thứ tự duyệt khóa không được bảo đảm. Hãy nhớ truyềnTreeMap::newkhi cần thứ tự cố định.
5.3. partitioningBy: chia hai nhóm đúng/sai
partitioningBy(Predicate) luôn cho Map<Boolean, ...> với đúng hai khóa true và false (kể cả nhóm rỗng). Rất hợp để chia đỗ/trượt, chẵn/lẻ.
List<Integer> nums = List.of(12, 7, 30, 45, 8, 51, 20);
Map<Boolean, List<Integer>> parts = nums.stream().collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println("Chẵn: " + parts.get(true));
System.out.println("Lẻ: " + parts.get(false));
Map<Boolean, Long> counts = nums.stream().collect(Collectors.partitioningBy(n -> n >= 20, Collectors.counting()));
System.out.println("Từ 20 trở lên: " + counts.get(true) + ", dưới 20: " + counts.get(false));5.4. Bảng chọn nhanh
| Bạn muốn | Dùng |
|---|---|
| Gom thành danh sách / tập | toList(), toSet() |
| Nối thành một chuỗi | joining(...) |
| Đếm theo nhóm | groupingBy(f, counting()) |
| Tổng/trung bình theo nhóm | groupingBy(f, summingInt(g)) / averagingDouble(g) |
| Chia hai nhóm theo điều kiện | partitioningBy(pred) |
| Tạo từ điển khóa → giá trị | toMap(k, v, merge, TreeMap::new) |
| Lấy phần tử lớn nhất mỗi nhóm | groupingBy(f, collectingAndThen(maxBy(cmp), Optional::get)) |
Ví dụ thực tế: Báo cáo cuối tháng của một cửa hàng gần như luôn là một
groupingBy: doanh thu theo loại hàng, số đơn theo ngày, khách chi tiêu nhiều nhất theo khu vực. Học thuần thụcCollectorslà học cách viết báo cáo trong vài dòng.
Vừa học xong collect, groupingBy và partitioningBy, hãy làm năm bài sau: từ nối chuỗi có ngoặc, nhóm theo độ dài, phân hoạch chẵn/lẻ cho đến đếm tần số từ; bài cuối khó nhất vì phải sắp xếp kết quả theo tần số.
6. Luồng song song (sơ lược)
Chỉ cần đổi stream() thành parallelStream() (hoặc thêm .parallel()), Java chia dữ liệu cho nhiều luồng của Fork/Join pool (buổi 9) xử lý rồi gộp kết quả. Tổng, đếm, max cho cùng kết quả như luồng thường, nhưng thứ tự thực thi từng phần tử không còn cố định.
// Chạy trên máy của bạn (IntelliJ/VS Code): hai kết quả luôn bằng nhau
long seq = LongStream.rangeClosed(1, 1_000_000).sum();
long par = LongStream.rangeClosed(1, 1_000_000).parallel().sum();
System.out.println(seq == par); // trueLưu ý: Môi trường chạy thử trên web giới hạn số luồng nên ví dụ trên không có nút Chạy thử; hãy gõ thử trong IDE trên máy bạn. Cộng là phép kết hợp nên kết quả song song luôn bằng kết quả tuần tự.
Nhưng luồng song song không phải lúc nào cũng nhanh hơn:
| Nên cân nhắc | Không nên |
|---|---|
| Dữ liệu rất lớn, mỗi phần tử tốn nhiều CPU | Dữ liệu nhỏ (chi phí chia việc lớn hơn lợi ích) |
| Phép toán độc lập, không trạng thái | Lambda sửa biến dùng chung (race condition như buổi 9) |
Nguồn dễ chia (mảng, ArrayList, range) |
Nguồn khó chia như LinkedList, iterate |
Cảnh báo: Không đưa
forEach(x -> list.add(x))vào luồng song song:ArrayListkhông an toàn luồng. Hãy đểcollectgom kết quả. Với dữ liệu nhỏ như trong các bài tập, cứ dùng luồng thường.
7. Tổng hợp: giải bài toán thực tế bằng pipeline
Khi gặp một đề bài, hãy tự hỏi theo thứ tự: nguồn là gì → cần lọc gì → biến đổi gì → kết thúc bằng gì? Ví dụ đề: "Cho danh sách đơn hàng (khách, số tiền). Tìm ba khách chi tiêu nhiều nhất, in tên: tổng". Đây là bài groupingBy rồi sorted rồi limit.
class Order {
String customer; double amount;
Order(String customer, double amount) { this.customer = customer; this.amount = amount; }
String getCustomer() { return customer; }
double getAmount() { return amount; }
}
public class Main {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("An", 120), new Order("Bình", 300), new Order("An", 80),
new Order("Chi", 150), new Order("Bình", 50), new Order("Dũng", 90));
Map<String, Double> total = orders.stream().collect(Collectors.groupingBy(
Order::getCustomer, TreeMap::new, Collectors.summingDouble(Order::getAmount)));
total.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(3)
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
}
}Hai điều đáng ghi nhớ: (1) tập entrySet() của một Map cũng là nguồn Stream được; (2) kiểu tường minh Map.Entry.<String, Double>comparingByValue() cần thiết để trình biên dịch suy luận đúng khi gọi .reversed().
Một mẫu tổng hợp nữa: phân loại theo nhiều mức bằng groupingBy lồng nhau.
List<String> rows = List.of("KTPM1 nam 8", "KTPM1 nu 9", "KHMT1 nam 7", "KHMT1 nam 5", "KTPM1 nam 6");
Map<String, Map<String, Double>> avg = rows.stream()
.map(r -> r.split(" "))
.collect(Collectors.groupingBy(a -> a[0], TreeMap::new,
Collectors.groupingBy(a -> a[1], TreeMap::new,
Collectors.averagingDouble(a -> Double.parseDouble(a[2])))));
System.out.println(avg);Phần cuối của bài tập là các đề trộn nhiều kỹ thuật: thống kê điểm theo lớp trên dữ liệu mô phỏng thực tế, làm phẳng nhiều danh sách bằng flatMap, và tìm người lương cao nhất mỗi phòng ban bằng collectingAndThen. Hãy tự vẽ pipeline ra giấy trước khi gõ.
8. Tổng kết khóa học
8.1. Bản đồ mười buổi
Nhìn lại toàn bộ hành trình: từ dòng Hello World ở buổi 1 đến những pipeline Stream hôm nay, bạn đã đi qua đủ chín chương của đề cương. Mỗi buổi đặt thêm một "tầng" lên tầng trước.
| Buổi | Chủ đề | Bạn mang theo được gì |
|---|---|---|
| 1 | JVM, biến, kiểu dữ liệu, vào/ra | javac/java, kiểu nguyên thủy, Scanner, toán tử |
| 2 | Chuỗi và cấu trúc điều khiển | String bất biến, StringBuilder, if/switch, các vòng lặp |
| 3 | Mảng, tìm kiếm, sắp xếp | Mảng là đối tượng, Arrays, tìm nhị phân, sắp xếp cơ bản |
| 4 | Phương thức và ngoại lệ | Hàm tĩnh, đệ quy, try/catch/finally, ngoại lệ tự định nghĩa |
| 5 | OOP (1): lớp và đối tượng | Hàm tạo, đóng gói, static, toString/equals/hashCode |
| 6 | OOP (2): kế thừa, đa hình | extends, lớp trừu tượng, interface, dynamic dispatch |
| 7 | Generics và Collections | List/Set/Map/Queue, Comparable/Comparator |
| 8 | Dòng dữ liệu và tệp | Reader/Writer, Files, try-with-resources, Serializable |
| 9 | Đa luồng | Thread, synchronized, ExecutorService, Future |
| 10 | Lambda, Stream | Giao diện hàm, Optional, pipeline, Collectors |
Có thể gom thành ba "mạch" xuyên suốt khóa: (1) mô hình bộ nhớ (stack, heap, tham chiếu — buổi 1, 3, 5, 7), (2) thiết kế phần mềm (hàm, lớp, kế thừa, interface, generics — buổi 4 đến 7) và (3) làm việc với thế giới bên ngoài (tệp, luồng, ngoại lệ, Stream — buổi 4, 8, 9, 10). Đa số lỗi người mới mắc đều quy về một trong ba mạch này.
Mẹo: Khi ôn, đừng đọc lại từ trang đầu. Hãy lấy một chương trình nhỏ (quản lý sinh viên chẳng hạn) và hỏi: "chỗ nào của chương trình này dùng kiến thức buổi nào?" — bạn sẽ thấy các buổi nối với nhau ra sao.
8.2. Gợi ý dự án nhóm cuối kỳ
Dự án nhóm 3–4 người, kéo dài 3–4 tuần, bắt buộc dùng đủ các nhóm kiến thức của khóa. Bạn có thể chọn một trong các đề dưới đây hoặc tự đề xuất (không dùng nghiệp vụ chứng khoán).
| Đề | Mô tả ngắn | Kiến thức then chốt |
|---|---|---|
| Quản lý thư viện | Sách, độc giả, mượn/trả, phạt trễ hạn | OOP, Collections, tệp, Stream báo cáo |
| Quản lý điểm sinh viên | Lớp, môn học, điểm; thống kê theo lớp/học kỳ | Kế thừa, Comparator, groupingBy, đọc/ghi CSV |
| Hệ thống đặt hàng cửa hàng | Sản phẩm, giỏ hàng, đơn hàng, khuyến mãi | Interface, lambda (quy tắc giảm giá), Optional |
| Mô phỏng ngân hàng | Tài khoản, giao dịch, nhiều khách cùng giao dịch | Ngoại lệ tự định nghĩa, synchronized, ExecutorService |
| Công cụ phân tích log/văn bản | Đọc tệp lớn, đếm từ, thống kê tần suất | BufferedReader, Stream, Map, đa luồng |
Mọi đề nên được làm theo một lộ trình giống nhau:
- Phân tích — viết 5–10 yêu cầu chức năng bằng một câu mỗi yêu cầu.
- Thiết kế lớp — gạch chân danh từ (lớp) và động từ (phương thức), vẽ sơ đồ lớp, quyết định đâu là
interface, đâu là lớp trừu tượng. - Cài đặt từng lát — mỗi lần một chức năng, chạy được rồi mới làm tiếp; dùng Git, mỗi người một nhánh.
- Kiểm thử — viết các hàm kiểm thử thủ công (hoặc JUnit) cho phần logic quan trọng; xử lý ngoại lệ cho mọi đầu vào của người dùng và tệp.
- Hoàn thiện & báo cáo — JavaDoc cho các lớp công khai, tệp
READMEhướng dẫn chạy, demo 10 phút.
Tiêu chí chấm gợi ý:
| Tiêu chí | Nội dung | Tỷ trọng |
|---|---|---|
| Thiết kế OOP | Lớp rõ trách nhiệm, đóng gói, dùng kế thừa/interface hợp lý | 30% |
| Chức năng | Chạy đúng, xử lý ngoại lệ và đầu vào sai | 30% |
| Java hiện đại | Collections, lambda/Stream, Optional đúng chỗ |
15% |
| Chất lượng mã | Đặt tên, chú thích, JavaDoc, không lặp mã | 15% |
| Làm việc nhóm & báo cáo | Lịch sử Git, phân công, demo | 10% |
8.3. Hướng ôn tập
Một kế hoạch ôn tập gọn cho một tuần trước kỳ thi:
| Ngày | Việc làm |
|---|---|
| 1 | Buổi 1–3: viết lại từ đầu 5 chương trình ngắn (nhập/xuất, chuỗi, vòng lặp, mảng, sắp xếp) không nhìn bài giảng |
| 2 | Buổi 4: hàm, đệ quy, tự vẽ stack cho factorial(4) và fib(5); làm lại các bài ngoại lệ |
| 3 | Buổi 5–6: vẽ sơ đồ lớp cho một bài toán mới; luyện đoán đầu ra các đoạn code đa hình |
| 4 | Buổi 7: chọn đúng List/Set/Map cho 10 tình huống; viết Comparator bằng lambda |
| 5 | Buổi 8–9: đọc/ghi tệp văn bản bằng try-with-resources; chỉ ra race condition trong đoạn mã cho trước |
| 6 | Buổi 10: đổi 5 vòng for quen thuộc thành Stream và ngược lại |
| 7 | Làm một đề tổng hợp tự ra trong 90 phút, chấm lại bằng chính bài tập trên Codelab |
Các dạng câu hỏi hay gặp: đoán đầu ra (chuỗi, tham chiếu, static, đa hình, thứ tự try/catch/finally), tìm lỗi (biên dịch hoặc logic), viết hàm/lớp ngắn theo mô tả, chọn cấu trúc dữ liệu kèm lý do, giải thích khái niệm (== và equals, ArrayList và LinkedList, Runnable và Callable, Collection và Stream).
Lưu ý: Hãy dùng chính ngân hàng bài tập của Codelab làm "ngân hàng đề": mỗi nhóm bài ở từng buổi là một bộ luyện tập riêng. Bài nào làm sai ở lần đầu, hãy ghi vào sổ lỗi và làm lại sau ba ngày.
8.4. Lộ trình học tiếp
Khóa này cho bạn nền tảng ngôn ngữ. Các hướng đi tiếp theo, từ gần đến xa:
| Giai đoạn | Chủ đề | Gợi ý |
|---|---|---|
| Củng cố | Cấu trúc dữ liệu & giải thuật bằng Java | Luyện thêm bài thuật toán, đọc mã nguồn của ArrayList, HashMap |
| Công cụ | Maven/Gradle, Git, JUnit 5, logging | Chuyển dự án nhóm sang Maven và viết bộ kiểm thử đơn vị |
| Cơ sở dữ liệu | JDBC, sau đó JPA/Hibernate | Kết nối dự án quản lý thư viện với cơ sở dữ liệu thật |
| Web/Backend | Spring Boot, REST API | Biến dự án thành dịch vụ web |
| Ngôn ngữ | Java 14–21: record, switch biểu thức, text block, sealed, luồng ảo |
Học các tính năng mới sau khi làm chủ Java 8–13 |
| Nâng cao | Mẫu thiết kế, kiến trúc phân lớp, đồng thời nâng cao | Đọc Effective Java của Joshua Bloch |
Mở rộng (Java 14+): Các phiên bản Java mới còn bổ sung
record(lớp dữ liệu bất biến chỉ một dòng), biểu thứcswitchdạng->, text block"""và so khớp mẫu choinstanceof. Chúng chỉ là cách viết gọn cho những ý bạn đã nắm vững trong khóa này, nên học chúng sẽ rất nhanh. Máy chấm của khóa dùng Java 13 nên các tính năng này chưa chạy được ở đây.
Lời khuyên cuối khóa: mỗi tuần viết một chương trình nhỏ của riêng bạn (trò chơi đoán số, sổ chi tiêu, trình đếm từ...). Lập trình giống chơi nhạc: đọc sách hiểu lý thuyết thôi chưa đủ, những giờ gõ phím mới biến kiến thức thành kỹ năng.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
| Dùng lại một Stream đã có phép kết thúc | IllegalStateException |
Tạo Stream mới từ nguồn mỗi lần |
Viết Stream mà quên phép kết thúc |
Không có gì chạy (tính lười), dễ tưởng đoạn mã bị bỏ qua | Luôn kết thúc bằng collect, forEach, count... |
Sửa biến cục bộ bên trong lambda (total += x) |
Lỗi biên dịch "effectively final" | Dùng reduce/sum, hoặc mapToInt(...).sum() |
Gọi Optional.get() không kiểm tra |
NoSuchElementException |
Dùng orElse, orElseGet, orElseThrow, ifPresent |
Dùng groupingBy mặc định rồi in theo thứ tự |
Thứ tự khóa không ổn định | Truyền TreeMap::new làm tham số thứ hai |
toMap khi khóa có thể trùng |
IllegalStateException: Duplicate key |
Thêm hàm trộn: toMap(k, v, (a, b) -> a) |
reduce với phép không kết hợp (như trừ) |
Kết quả sai, đổi khi chạy song song | Dùng phép kết hợp (cộng, nhân, max) |
Nhầm map với flatMap |
Ra Stream<List<T>> thay vì Stream<T> |
Dùng flatMap khi mỗi phần tử tự là một tập |
Cho parallelStream() ghi vào ArrayList chung |
Mất dữ liệu, ngoại lệ ngẫu nhiên | Dùng collect, hoặc dùng luồng thường |
Lạm dụng peek hoặc tác dụng phụ trong map/filter |
Kết quả khó đoán, khó gỡ lỗi | Chỉ dùng peek để quan sát, logic đặt ở đúng phép |
| Ép dùng tham chiếu phương thức khi lambda rõ hơn | Mã khó đọc | Chọn cách viết dễ đọc nhất |
Dùng average() hay max() mà bỏ qua Optional trả về |
Lỗi biên dịch hoặc chạy khi luồng rỗng | Xử lý bằng orElse(0) / getAsInt() có kiểm tra |
Tóm tắt
- Giao diện hàm có đúng một phương thức trừu tượng; lambda
(a, b) -> a + blà cách viết gọn đối tượng của nó; biến bắt giữ phải effectively final. - Sáu giao diện chuẩn:
Function(biến đổi),Predicate(kiểm tra),Supplier(cung cấp),Consumer(tiêu thụ),UnaryOperator,BinaryOperator; ghép bằngandThen,and,negate. - Tham chiếu phương thức
Lớp::phươngThứclà lambda rút gọn khi chỉ gọi một hàm có sẵn. Optionalnói rõ "có thể vắng": dùngmap,filter,orElse,ifPresentthay vì kiểm tranull.- Stream = nguồn → phép trung gian (lười) → phép kết thúc; không sửa nguồn và chỉ dùng một lần.
- Trung gian:
filter,map,flatMap,sorted,distinct,limit,skip. Kết thúc:count,reduce,findFirst,anyMatch,collect, thống kê củaIntStream. Collectors:toList,joining,groupingBy(+ collector hạ nguồn),partitioningBy,toMap; dùngTreeMap::newkhi cần thứ tự ổn định.- Luồng song song chỉ đáng dùng với dữ liệu lớn và phép toán độc lập, không trạng thái.
- Khóa học: mười buổi đi từ JVM đến Stream; việc tiếp theo là dự án nhóm, ôn tập có kế hoạch và học tiếp Maven, JDBC, Spring Boot.