Bỏ qua điều hướng
EN
Tất cả bài viết

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

Ôn tập bài nàyThẻ ghi nhớ & câu hỏi ôn tập7 thẻ ghi nhớ · 6 câu trắc nghiệm
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ột Comparator — 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):

LambdaCoBan.java
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):

LambdaBienNgoai.java
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:

InterfacePhương thứcÝ nghĩaVí dụ
Predicate<T>boolean test(T)Điều kiệns -> s.isBlank()
Function<T, R>R apply(T)Biến đổi T thành Rs -> s.length()
Consumer<T>void accept(T)Làm gì đó với Ts -> 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ểus -> s.trim()
HamCoSan.java
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 :::

LambdaTham 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ý:

Đường ống stream

Đ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.
StreamCoBan.java
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:

ThuThapKetQua.java
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

TaoStream.java
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ị":

OptionalDemo.java
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ênTránh
Dùng Optional làm kiểu trả về của phương thức tìm kiếmDùng Optional làm tham số, thuộc tính, phần tử collection
map, orElse, orElseGet, orElseThrow, ifPresentopt.get() không kiểm tra — y như dùng null
Trả về collection rỗng thay vì Optional<List<…>> hay nullif (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 break sớ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

  1. 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).
  2. Đọc file văn bản (bài 10) bằng Files.lines, in 10 từ dài nhất không trùng.
  3. Viết Optional<SanPham> timReNhat(List<SanPham> ds, String danhMuc) và in kết quả hoặc "Không có sản phẩm".
  4. 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, Supplier có sẵn; ghép bằng and, 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, summingLong thay cho vòng lặp gom dữ liệu.
  • Optional cho kết quả có thể vắng mặt; dùng map/orElse/orElseThrow, tránh get() 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.

Đang tải câu hỏi luyện tập…

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.

Đang tải đánh giá…
Chọn số sao từ 1 đến 5

Bình luận

Đăng nhập để bình luận không cần nhập tên và được duyệt nhanh hơn.

0/2000
Đang tải bình luận…

Đọc tiếp

Bài số № 55

Java cơ bản #12: dự án cuối — ứng dụng quản lý chi tiêu với Maven và JUnit

Gom toàn bộ kiến thức vào một dự án thật: cấu trúc Maven và pom.xml, chia lớp model–kho dữ liệu–service–giao diện, lưu file CSV, thống kê bằng Stream, cảnh báo ngân sách, viết kiểm thử JUnit 5, đóng gói file jar chạy được — và lộ trình học tiếp sau Java cơ bản.

19 phút đọc

Bài số № 53

Java cơ bản #10: xử lý ngoại lệ và đọc ghi file với java.nio.file

Khi chương trình gặp lỗi: đọc stack trace, try/catch/finally, checked và unchecked exception, throw và throws, tự định nghĩa ngoại lệ nghiệp vụ, try-with-resources tự đóng tài nguyên; đọc ghi file văn bản và CSV bằng Files, Path, UTF-8 — kèm các thói quen xử lý lỗi nên tránh.

15 phút đọc

Bài số № 52

Java cơ bản #9: Collections — List, Set, Map, Generics và sắp xếp với Comparator

Bộ sưu tập co giãn thay cho mảng: ArrayList, HashSet, HashMap và khi nào dùng loại nào; generics và kiểu bao (Integer), duyệt và xoá an toàn, danh sách bất biến List.of, sắp xếp bằng Comparable/Comparator, Deque làm ngăn xếp và hàng đợi, cùng độ phức tạp của từng thao tác.

16 phút đọc