CodeGym /Các khóa học /JAVA 25 SELF /Sử dụng lambda trong collections và streams

Sử dụng lambda trong collections và streams

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Biểu thức lambda và collections

Hãy nhớ lại cách xử lý collections trước khi có biểu thức lambda. Giả sử ta có một danh sách chuỗi và muốn in ra màn hình:

List<String> list = Arrays.asList("mèo", "chó", "nhím");

for (String s : list) {
    System.out.println(s);
}

Mọi thứ khá đơn giản, nhưng nếu muốn xóa tất cả chuỗi rỗng khỏi danh sách, ta phải viết vòng lặp với điều kiện, đôi khi còn phải dùng iterator (nếu không sẽ có ConcurrentModificationException). Hoặc, chẳng hạn, sắp xếp theo độ dài chuỗi:

Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Ngay cả với tác vụ đơn giản như vậy — đã 5 dòng mã và đầy dấu ngoặc “ồn ào”. Tối ưu thế nào? Câu trả lời bạn đã biết: biểu thức lambda.

Ứng dụng biểu thức lambda trong các phương thức của collection

Từ Java 8, các interface của collections có thêm những phương thức nhận functional interface — nghĩa là ta có thể truyền biểu thức lambda vào. Dưới đây là những phương thức phổ biến:

  • forEach(Consumer<T> action)
  • removeIf(Predicate<T> filter)
  • sort(Comparator<T> c)
  • replaceAll(UnaryOperator<T> operator)

Ví dụ: forEach

In tất cả phần tử của danh sách ra màn hình (cách cũ):

for (String s : list) {
    System.out.println(s);
}

Bây giờ — với lambda:

list.forEach(s -> System.out.println(s));

Hoặc ngắn hơn nữa, nếu muốn “làm cao thủ Java”:

list.forEach(System.out::println); // method reference, sẽ bàn sau

Ví dụ: removeIf

Xóa tất cả chuỗi rỗng khỏi danh sách:

List<String> animals = new ArrayList<>(Arrays.asList("mèo", "", "chó", "nhím", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [mèo, chó, nhím]

Ví dụ: sort

Sắp xếp danh sách theo độ dài chuỗi:

List<String> animals = Arrays.asList("mèo", "chó", "nhím", "voi");

animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [mèo, chó, voi, nhím]

Ví dụ: replaceAll

Chuyển tất cả chuỗi sang chữ hoa:

List<String> animals = new ArrayList<>(Arrays.asList("mèo", "chó", "nhím"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [MÈO, CHÓ, NHÍM]

2. Stream API và biểu thức lambda

Từ Java 8, Stream API xuất hiện — một công cụ mạnh để xử lý collections theo phong cách hàm. Stream cho phép lọc, biến đổi, sắp xếp, thu thập collections thông qua chuỗi phương thức. Và tất cả các phương thức này nhận biểu thức lambda!

Quan trọng: Phần phân tích đầy đủ về Stream API sẽ ở sau; hiện tại — chỉ các ví dụ cơ bản để hiểu vai trò của lambda.

Ví dụ: lọc

Giữ lại các chuỗi dài hơn 3 ký tự:

List<String> animals = Arrays.asList("mèo", "voi", "nhím", "cá sấu");
animals.stream()
       .filter(s -> s.length() > 3)
       .forEach(System.out::println); // nhím, cá sấu

Ví dụ: biến đổi (map)

Chuyển tất cả chuỗi sang chữ hoa:

List<String> animals = Arrays.asList("mèo", "voi", "nhím");
List<String> upper = animals.stream()
                            .map(s -> s.toUpperCase())
                            .collect(Collectors.toList());
System.out.println(upper); // [MÈO, VOI, NHÍM]

Ví dụ: sắp xếp

Lấy danh sách được sắp xếp theo độ dài (không thay đổi danh sách gốc):

List<String> animals = Arrays.asList("mèo", "voi", "nhím", "cá sấu");
List<String> sorted = animals.stream()
                             .sorted((a, b) -> a.length() - b.length())
                             .collect(Collectors.toList());
System.out.println(sorted); // [mèo, voi, nhím, cá sấu]

3. So sánh với lớp ẩn danh

Hãy so sánh cùng một đoạn mã khi dùng lớp ẩn danh và khi dùng lambda.

Sắp xếp theo độ dài chuỗi

Lớp ẩn danh:

animals.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Biểu thức lambda:

animals.sort((a, b) -> a.length() - b.length());

Kết luận:
Biểu thức lambda tiết kiệm rất nhiều dòng và làm mã dễ đọc hơn. Ít dấu ngoặc, ít “nhiễu” — nhiều “cốt lõi” hơn!

Xóa các chuỗi rỗng

Lớp ẩn danh:

animals.removeIf(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.isEmpty();
    }
});

Biểu thức lambda:

animals.removeIf(s -> s.isEmpty());

4. Thực hành: bài tập ngắn với biểu thức lambda

Hãy thử áp dụng biểu thức lambda trong một mini-program làm việc với danh sách người dùng.

Ví dụ 1: Lọc người dùng theo tuổi

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

List<User> users = Arrays.asList(
    new User("Alisa", 17),
    new User("Bob", 25),
    new User("Charli", 15)
);

users.stream()
     .filter(u -> u.age >= 18)
     .forEach(System.out::println); // Bob (25)

Ví dụ 2: Sắp xếp người dùng theo tên

List<User> users = Arrays.asList(
    new User("Alisa", 17),
    new User("Bob", 25),
    new User("Charli", 15)
);

users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alisa (17), Bob (25), Charli (15)]

Ví dụ 3: Chuyển danh sách người dùng thành danh sách tên

List<String> names = users.stream()
                          .map(u -> u.name)
                          .collect(Collectors.toList());
System.out.println(names); // [Alisa, Bob, Charli]

Ví dụ 4: Xóa tất cả người vị thành niên

List<User> users = new ArrayList<>(Arrays.asList(
    new User("Alisa", 17),
    new User("Bob", 25),
    new User("Charli", 15)
));

users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]

5. Những lưu ý hữu ích

Đặc điểm: phạm vi và biến “effectively final

Biểu thức lambda có thể dùng biến từ phương thức bên ngoài, nhưng chỉ khi chúng là final hoặc “effectively final” (tức là không bị thay đổi sau khi khởi tạo).

int minAge = 18;
users.stream()
     .filter(u -> u.age >= minAge)
     .forEach(System.out::println);

Nếu bạn cố thay đổi minAge sau khi dùng trong lambda — trình biên dịch sẽ báo lỗi.

Bảng: các phương thức chính của collections và streams với biểu thức lambda

Phương thức của collection/stream Chức năng Kiểu biểu thức lambda Ví dụ
forEach(Consumer<T>)
Cho từng phần tử
x -> ...
list.forEach(s -> ...)
removeIf(Predicate<T>)
Xóa phần tử theo điều kiện
x -> ...
list.removeIf(s -> ...)
sort(Comparator<T>)
Sắp xếp các phần tử
(a, b) -> ...
list.sort((a, b) -> ...)
replaceAll(UnaryOperator<T>)
Thay thế từng phần tử
x -> ...
list.replaceAll(s -> ...)
filter(Predicate<T>)
Lọc stream
x -> ...
stream.filter(s -> ...)
map(Function<T, R>)
Biến đổi phần tử
x -> ...
stream.map(s -> ...)
forEach(Consumer<T>)
Duyệt stream
x -> ...
stream.forEach(s -> ...)
sorted(Comparator<T>)
Sắp xếp trong stream
(a, b) -> ...
stream.sorted((a, b) -> ...)

7. Lỗi thường gặp

Lỗi №1: Lambda quá dài. Nếu bên trong biểu thức lambda của bạn đã có 5 dòng mã, điều kiện, vòng lặp và try-catch — rất có thể nên tách đoạn mã đó ra một phương thức riêng. Lambda phù hợp cho logic ngắn gọn.

Lỗi №2: Sử dụng biến có thể thay đổi. Nếu bạn cố gắng bên trong lambda thay đổi biến từ phương thức bên ngoài (ví dụ một biến đếm), trình biên dịch sẽ không cho phép. Biến phải là final hoặc hiệu quả là final.

Lỗi №3: Quên rằng các phương thức của collection/stream không phải lúc nào cũng thay đổi collection gốc. Ví dụ, stream().filter(...) không thay đổi danh sách ban đầu, mà trả về một stream mới. Nếu muốn nhận về collection — hãy dùng collect(Collectors.toList()).

Lỗi №4: Biểu thức lambda không khớp kiểu. Nếu phương thức nhận, chẳng hạn, Comparator<T>, nhưng bạn lại truyền lambda chỉ có một tham số (thay vì hai) — sẽ xảy ra lỗi biên dịch.

Lỗi №5: Mất tính dễ đọc khi lồng nhiều lambda. Nếu bạn có một chuỗi map, filter, forEach, và bên trong mỗi lambda lại có thêm một lambda — mã sẽ trở nên khó đọc. Trong những trường hợp như vậy, tốt hơn nên tách biểu thức thành từng bước hoặc đưa một số phần vào các phương thức riêng.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION