Java cơ bản #11: lambda, tham chiếu phương thức, Stream API và Optional
Lập trình kiểu hàm trong Java: lambda và functional interface (Predicate, Function, Consumer, Supplier), tham chiếu phương thức ::, Stream với filter–map–reduce, sorted, collect, groupingBy, Optional thay cho null — cùng lời khuyên khi nào vòng lặp vẫn tốt hơn stream.
10 phút đọc
Mục lục
So sánh hai cách lấy tên các đơn hàng trên 1 triệu, sắp theo giá giảm dần:
- Vòng lặp: tạo danh sách rỗng, duyệt,
if, thêm vào, rồi sắp xếp với mộtComparator— mười dòng nói làm thế nào. - Stream:
donHang.stream().filter(...).sorted(...).map(...).toList()— một dòng nói muốn gì.
Từ Java 8, lambda và Stream API mang phong cách lập trình hàm vào Java. Code xử lý dữ liệu trong các dự án Java hiện đại (và trong Spring) dùng chúng khắp nơi; bài này giúp bạn đọc và viết thành thạo.
Lambda
Lambda là một hàm vô danh viết gọn: (tham số) -> biểu thức. Nó cài đặt một functional interface — interface có đúng một phương thức trừu tượng (đã gặp ở bài 7):
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
void main() {
List<String> ten = new ArrayList<>(List.of("Thảo", "An", "Khánh", "Bình"));
// Trước Java 8: lớp vô danh (anonymous class)
ten.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
// Lambda — cùng ý nghĩa
ten.sort((a, b) -> Integer.compare(a.length(), b.length()));
IO.println(ten);
// Thân nhiều lệnh: dùng { } và return
ten.sort((a, b) -> {
int theoDoDai = Integer.compare(b.length(), a.length());
return theoDoDai != 0 ? theoDoDai : a.compareTo(b);
});
IO.println(ten);
ten.forEach(t -> IO.println("Xin chào " + t));
}Lambda dùng được biến bên ngoài nếu biến đó không bị gán lại (effectively final):
import java.util.List;
void main() {
int nguong = 5; // không gán lại sau đó → dùng được trong lambda
List<Integer> so = List.of(3, 7, 1, 9);
so.stream().filter(x -> x > nguong).forEach(IO::println);
// nguong = 6; → nếu có dòng này, lambda phía trên báo lỗi biên dịch
}Các functional interface có sẵn
Gói java.util.function có sẵn những "hình dạng" hàm hay dùng — không cần tự định nghĩa:
| Interface | Phương thức | Ý nghĩa | Ví dụ |
|---|---|---|---|
Predicate<T> | boolean test(T) | Điều kiện | s -> s.isBlank() |
Function<T, R> | R apply(T) | Biến đổi T thành R | s -> s.length() |
Consumer<T> | void accept(T) | Làm gì đó với T | s -> IO.println(s) |
Supplier<T> | T get() | Tạo ra T | () -> new ArrayList<>() |
BiFunction<T, U, R> | R apply(T, U) | Hàm hai tham số | (a, b) -> a + b |
UnaryOperator<T> | T apply(T) | Biến đổi cùng kiểu | s -> s.trim() |
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
void main() {
Predicate<String> laEmail = s -> s.contains("@");
Predicate<String> laGmail = laEmail.and(s -> s.endsWith("@gmail.com")); // ghép điều kiện
Function<String, String> chuanHoa = String::strip;
Function<String, String> chuanHoaThuong = chuanHoa.andThen(String::toLowerCase); // ghép hàm
Supplier<Long> thoiGian = System::currentTimeMillis;
String nhap = " An.Nguyen@Gmail.com ";
String email = chuanHoaThuong.apply(nhap);
IO.println(email + " | email: " + laEmail.test(email) + " | gmail: " + laGmail.test(email));
IO.println(thoiGian.get() > 0);
}Tham chiếu phương thức ::
Khi lambda chỉ gọi một phương thức có sẵn, viết gọn bằng :::
| Lambda | Tham chiếu phương thức |
|---|---|
s -> s.toUpperCase() | String::toUpperCase |
x -> IO.println(x) | IO::println |
s -> Integer.parseInt(s) | Integer::parseInt |
() -> new ArrayList<>() | ArrayList::new |
sv -> sv.diem() | SinhVien::diem |
Stream API
Một stream là chuỗi phần tử đi qua một đường ống các bước xử lý:
Đang vẽ sơ đồ…
flowchart LR
A["Nguồn<br/>list.stream()"] --> B["filter<br/>(giữ lại)"]
B --> C["map<br/>(biến đổi)"]
C --> D["sorted<br/>(sắp xếp)"]
D --> E["Thao tác cuối<br/>toList / count / sum / collect"]- Thao tác trung gian (
filter,map,sorted,distinct,limit…) trả về stream mới và lười: chưa chạy gì cả. - Thao tác cuối (
toList,count,forEach,collect,reduce…) kích hoạt cả đường ống và cho ra kết quả. - Stream không sửa nguồn dữ liệu, và chỉ dùng được một lần.
import java.util.List;
record DonHang(String ma, String khach, String danhMuc, long tongTien) {}
void main() {
List<DonHang> ds = List.of(
new DonHang("D1", "An", "điện tử", 12_500_000),
new DonHang("D2", "Bình", "thời trang", 450_000),
new DonHang("D3", "An", "sách", 320_000),
new DonHang("D4", "Chi", "điện tử", 3_200_000),
new DonHang("D5", "Bình", "điện tử", 990_000),
new DonHang("D6", "Chi", "thời trang", 1_750_000));
// Mã các đơn trên 1 triệu, giá giảm dần
List<String> donLon = ds.stream()
.filter(d -> d.tongTien() > 1_000_000)
.sorted((a, b) -> Long.compare(b.tongTien(), a.tongTien()))
.map(DonHang::ma)
.toList();
IO.println(donLon); // [D1, D4, D6]
long doanhThu = ds.stream().mapToLong(DonHang::tongTien).sum();
double tbDienTu = ds.stream().filter(d -> d.danhMuc().equals("điện tử"))
.mapToLong(DonHang::tongTien).average().orElse(0);
IO.println("Doanh thu %,d, TB điện tử %,.0f".formatted(doanhThu, tbDienTu));
List<String> khachHang = ds.stream().map(DonHang::khach).distinct().sorted().toList();
IO.println(khachHang); // [An, Bình, Chi]
boolean coDonNho = ds.stream().anyMatch(d -> d.tongTien() < 500_000);
boolean tatCaCoMa = ds.stream().allMatch(d -> d.ma().startsWith("D"));
IO.println(coDonNho + " " + tatCaCoMa + " " + ds.stream().filter(d -> d.khach().equals("An")).count());
}collect và groupingBy
Collectors gom kết quả thành Map, chuỗi, thống kê — thay cho cả chục dòng vòng lặp với HashMap:
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
record DonHang(String khach, String danhMuc, long tongTien) {}
void main() {
List<DonHang> ds = List.of(
new DonHang("An", "điện tử", 12_500_000), new DonHang("Bình", "thời trang", 450_000),
new DonHang("An", "sách", 320_000), new DonHang("Chi", "điện tử", 3_200_000),
new DonHang("Bình", "điện tử", 990_000), new DonHang("Chi", "thời trang", 1_750_000));
// Tổng chi tiêu theo khách hàng
Map<String, Long> theoKhach = ds.stream()
.collect(Collectors.groupingBy(DonHang::khach, TreeMap::new, Collectors.summingLong(DonHang::tongTien)));
IO.println(theoKhach);
// Số đơn theo danh mục
Map<String, Long> soDon = ds.stream().collect(Collectors.groupingBy(DonHang::danhMuc, Collectors.counting()));
IO.println(new TreeMap<>(soDon));
// Chia hai nhóm theo điều kiện
Map<Boolean, List<String>> chia = ds.stream().collect(Collectors.partitioningBy(
d -> d.tongTien() >= 1_000_000, Collectors.mapping(DonHang::khach, Collectors.toList())));
IO.println("Đơn lớn: " + chia.get(true) + " | đơn nhỏ: " + chia.get(false));
// Nối thành chuỗi
IO.println(ds.stream().map(DonHang::danhMuc).distinct().collect(Collectors.joining(", ", "[", "]")));
// Thống kê một lượt
var tk = ds.stream().mapToLong(DonHang::tongTien).summaryStatistics();
IO.println("min %,d · max %,d · TB %,.0f".formatted(tk.getMin(), tk.getMax(), tk.getAverage()));
}Tạo stream và reduce
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
void main() {
IO.println(IntStream.rangeClosed(1, 10).sum()); // 55
IO.println(IntStream.rangeClosed(1, 5).mapToObj(i -> i * i).toList()); // [1, 4, 9, 16, 25]
IO.println(Arrays.stream(new int[]{4, 8, 15, 16, 23, 42}).max().getAsInt()); // 42
IO.println(Stream.of("a", "b", "c").map(String::toUpperCase).toList());
IO.println(Stream.iterate(1, x -> x * 2).limit(10).toList()); // lũy thừa của 2
IO.println("Java Stream".chars().filter(Character::isUpperCase).count()); // 2
int tich = IntStream.rangeClosed(1, 5).reduce(1, (a, b) -> a * b); // reduce: gộp thành một giá trị
IO.println("5! = " + tich);
}Optional: thay cho null
Phương thức "tìm" có thể không tìm thấy. Trả về null thì nơi gọi dễ quên kiểm tra → NullPointerException. Optional<T> nói rõ trong kiểu trả về: "có thể không có giá trị":
import java.util.List;
import java.util.Optional;
record NguoiDung(String email, String ten, String thanhPho) {}
final List<NguoiDung> nguoiDung = List.of(
new NguoiDung("an@x.vn", "An", "Hà Nội"),
new NguoiDung("binh@x.vn", "Bình", null));
Optional<NguoiDung> timTheoEmail(String email) {
return nguoiDung.stream().filter(u -> u.email().equals(email)).findFirst();
}
void main() {
// Lấy giá trị kèm phương án dự phòng
String ten = timTheoEmail("chi@x.vn").map(NguoiDung::ten).orElse("Khách");
IO.println("Xin chào " + ten);
// Chỉ làm khi có giá trị
timTheoEmail("an@x.vn").ifPresent(u -> IO.println("Tìm thấy " + u.ten()));
// Chuỗi các bước có thể thiếu: không cần if lồng nhau
String tp = timTheoEmail("binh@x.vn")
.map(NguoiDung::thanhPho) // thanhPho null → Optional rỗng
.orElse("chưa cập nhật");
IO.println("Thành phố: " + tp);
// Không có giá trị là lỗi thật sự → ném ngoại lệ rõ nghĩa
try {
timTheoEmail("x@x.vn").orElseThrow(() -> new IllegalArgumentException("Không có người dùng x@x.vn"));
} catch (IllegalArgumentException e) {
IO.println(e.getMessage());
}
}| Nên | Tránh |
|---|---|
Dùng Optional làm kiểu trả về của phương thức tìm kiếm | Dùng Optional làm tham số, thuộc tính, phần tử collection |
map, orElse, orElseGet, orElseThrow, ifPresent | opt.get() không kiểm tra — y như dùng null |
Trả về collection rỗng thay vì Optional<List<…>> hay null | if (opt.isPresent()) { opt.get()... } khi map/ifPresent gọn hơn |
Stream hay vòng lặp?
Stream không phải lúc nào cũng tốt hơn:
- Dùng stream cho chuỗi biến đổi dữ liệu rõ ràng: lọc, biến đổi, gom nhóm, thống kê.
- Dùng vòng lặp khi cần
breaksớm phức tạp, sửa nhiều biến bên ngoài, xử lý ngoại lệ checked trong từng bước, hoặc logic có nhiều nhánh — ép vào stream làm code khó đọc hơn. - Lambda không nên có tác dụng phụ (sửa biến/collection bên ngoài) trong
map/filter. parallelStream()không tự động nhanh hơn; chỉ cân nhắc với dữ liệu rất lớn, tính toán nặng, và sau khi đo.
Bài tập
- Từ danh sách sinh viên (tên, lớp, điểm): in điểm trung bình theo lớp, sinh viên điểm cao nhất mỗi lớp (
groupingBy+maxBy). - Đọc file văn bản (bài 10) bằng
Files.lines, in 10 từ dài nhất không trùng. - Viết
Optional<SanPham> timReNhat(List<SanPham> ds, String danhMuc)và in kết quả hoặc "Không có sản phẩm". - Viết lại bằng stream ba bài tập vòng lặp ở bài 3–4 (tổng số chẵn, đếm nguyên âm, FizzBuzz) và so sánh độ dễ đọc.
Tổng kết
- Lambda
(x) -> ...cài đặt functional interface; dùng được biến ngoài effectively final. Predicate,Function,Consumer,Suppliercó sẵn; ghép bằngand,andThen; tham chiếu phương thức::khi lambda chỉ gọi một phương thức.- Stream: nguồn → thao tác trung gian (lười) → thao tác cuối; không sửa nguồn, dùng một lần.
Collectors.groupingBy,partitioningBy,joining,summingLongthay cho vòng lặp gom dữ liệu.Optionalcho kết quả có thể vắng mặt; dùngmap/orElse/orElseThrow, tránhget()mù quáng.- Bài cuối: gom mọi thứ vào một dự án thật với Maven và JUnit.
Nguồn tham khảo: dev.java — "Lambda Expressions", "The Stream API"; tài liệu API Java 25 — java.util.stream, java.util.function, java.util.Optional; Bloch — Effective Java, mục 42–48 (lambda và stream), 55 (Optional); Urma, Fusco, Mycroft — Modern Java in Action.
Bài này thế nào?
Bài viết có hữu ích với bạn?
Đánh giá giúp mình biết nên viết thêm về chủ đề nào.