CodeGym /Các khóa học /JAVA 25 SELF /Giới thiệu về Stream API: vì sao cần các stream

Giới thiệu về Stream API: vì sao cần các stream

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

1. Vấn đề của cách tiếp cận mệnh lệnh

Hãy bắt đầu với một bài toán kinh điển. Giả sử chúng ta có một danh sách người dùng:

List<User> users = ...; // đã được khởi tạo sẵn

Giả sử cần lấy danh sách tên của tất cả người dùng đã đủ 18 tuổi. Trước đây ta làm thế nào?

List<String> names = new ArrayList<>();
for (User user : users) {
    if (user.getAge() >= 18) {
        names.add(user.getName());
    }
}

Thoạt nhìn thì đơn giản, nhưng hãy để ý các bước “kỹ thuật”: khai báo danh sách kết quả, duyệt qua mọi phần tử, kiểm tra điều kiện, thêm kết quả.

Nếu bài toán phức tạp hơn — ví dụ cần lấy danh sách email duy nhất của tất cả người dùng có tên bắt đầu bằng chữ “A”, được sắp xếp theo thứ tự chữ cái — thì mã sẽ phình to rất nhanh: lọc, trích xuất trường, loại bỏ trùng lặp, sắp xếp, có thể còn chuyển đổi định dạng email, v.v. Kết quả là thay vì logic ngắn gọn, dễ hiểu, bạn nhận được một đống boilerplate khó đọc và khó bảo trì.

Nhược điểm của cách tiếp cận này gói gọn trong hai ý: nhiều mã lặp lại, xác suất mắc lỗi cao (quên sắp xếp hoặc xử lý trùng lặp sai), và khó kết hợp các thao tác. Đây gọi là phong cách mệnh lệnh: bạn mô tả cho máy tính cách thực hiện từng bước, chứ không phải điều bạn muốn nhận được.

2. Stream API — phong cách khai báo

Kể từ Java 8, Stream API xuất hiện — một công cụ cho phép làm việc với collection theo phong cách “cần làm gì”, thay vì “làm như thế nào”. Cách tiếp cận này gọi là khai báo.

Vậy Stream là gì?
Nó không phải một collection riêng, mà là một luồng dữ liệu: một dãy phần tử đi qua chuỗi thao tác — lọc, biến đổi, sắp xếp, thu thập kết quả, v.v. Stream không lưu trữ gì; nó chỉ “đưa” dữ liệu đi qua dây chuyền. Các thao tác có thể kết hợp gần như như LEGO: ráp chuỗi rồi chạy.

Ví dụ:

List<String> names = users.stream()                // tạo stream từ danh sách người dùng
    .filter(u -> u.getAge() >= 18)                 // chỉ giữ những người đã đủ 18 tuổi 
    .map(User::getName)                            // chuyển User → String (lấy tên)
    .collect(Collectors.toList());                 // thu thập kết quả thành List

Thế là xong — chỉ một dòng là mô tả toàn bộ bài toán: “lấy người dùng, lọc theo tuổi, lấy tên, gom vào danh sách”. Không cần tự viết vòng lặp, tạo biến tạm và canh chừng để không bỏ sót bước nào.

Stream API hoạt động như dây chuyền trong nhà máy: bạn định nghĩa các công đoạn xử lý, dữ liệu tự đi qua. Gọn gàng, súc tích và dễ đọc hơn nhiều lần.

3. Ưu điểm của Stream API

  • Ngắn gọn và dễ đọc. Hãy so sánh hai cách tiếp cận — mệnh lệnh và khai báo — trên cùng một bài toán. Sẽ thấy ngay cách nào dễ đọc và dễ bảo trì hơn.
  • Kết hợp thao tác dễ dàng. Dễ “xếp” các thao tác thành chuỗi: filter, map, sorted, collect — tất cả trong một mạch liền mạch.
  • Ít lỗi hơn. Stream API loại bỏ việc vặt: không cần tự quản lý collection trung gian và vòng lặp — giảm khả năng quên bước hoặc lỗi chỉ số.
  • Khả năng song song. Dễ mở rộng xử lý lên nhiều nhân chỉ bằng cách gọi parallelStream() thay cho stream(). Chi tiết — ở phần sau.
  • Phong cách lập trình hiện đại. Các ý tưởng của lập trình hàm (kết hợp hàm, không dùng vòng lặp tường minh) tăng tính biểu đạt và được đánh giá cao trên thị trường.

Lược sử Stream API

Stream API xuất hiện trong Java 8 (2014) và trở thành bước nhảy vọt cho ngôn ngữ. Trước đó, bất kỳ thao tác nào hơi phức tạp trên collection đều cần nhiều mã phụ trợ, trong khi các nền tảng khác đã cung cấp cách tiếp cận khai báo (map, filter, reduce, v.v.).

Kể từ đó, Stream API là tiêu chuẩn de facto cho xử lý collection trong Java. Nếu bạn muốn viết mã Java hiện đại — không thể thiếu nó.

4. Các lĩnh vực áp dụng Stream API

  • Lọc (filter): chọn ra các phần tử cần thiết (ví dụ, tất cả người dùng trên 18 tuổi).
  • Biến đổi (map): trích xuất trường hoặc tạo đối tượng mới (ví dụ, danh sách tên người dùng).
  • Gộp/Thống kê (aggregate): tính tổng, trung bình, đếm, lớn nhất/nhỏ nhất.
  • Sắp xếp (sort): sắp xếp phần tử theo tiêu chí mong muốn.
  • Nhóm (group): phân chia phần tử theo nhóm.
  • Thu thập kết quả (collect): gom vào danh sách, tập hợp, map, chuỗi, v.v.

Ví dụ bài toán:

  • Lấy danh sách email của tất cả người dùng có tên bắt đầu bằng “A”.
  • Tính tuổi trung bình của người dùng.
  • Tìm người dùng đầu tiên có email xác định.
  • Gom tất cả các thành phố sinh sống duy nhất vào một tập hợp.

5. Những điểm hữu ích

Cách một stream hoạt động

[User1] --\
[User2] ---|--> [ filter ] --> [ map ] --> [ collect ] --> List<String>
[User3] --/

1. filter — chỉ cho qua người dùng thỏa điều kiện.
2. map — chuyển người dùng thành, ví dụ, email.
3. collect — gom email vào collection mong muốn.

Cú pháp: tạo stream như thế nào

  • Từ collection: list.stream() — stream của các phần tử trong collection.
  • Từ mảng: Arrays.stream(array)
  • Từ các giá trị riêng lẻ: Stream.of("a", "b", "c")

6. Lỗi thường gặp khi chuyển sang Stream API

Lỗi №1: cố gắng thay đổi collection bên trong stream. Stream API không thiết kế để chỉnh sửa collection gốc (ví dụ, không nên gọi list.remove() trong forEach). Để xóa phần tử, hãy dùng removeIf trên collection trước hoặc sau khi xử lý.

Lỗi №2: nhầm lẫn giữa collection và stream. Stream không phải collection! Stream “đi qua” các phần tử một lần, sau đó đóng. Nếu cần xử lý lại dữ liệu — hãy tạo stream() mới.

Lỗi №3: chuỗi thao tác quá phức tạp. Đừng biến biểu thức thành “quái vật” với cả chục thao tác. Nếu chuỗi trở nên quá dài — hãy tách thành vài bước với biến trung gian để dễ đọc và dễ gỡ lỗi.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Theo dõi tiến trình trong trò chơi 🎮
Theo dõi tiến trình trong trò chơi 🎮
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Phân loại sản phẩm cho cửa hàng trực tuyến 🏷️
Phân loại sản phẩm cho cửa hàng trực tuyến 🏷️
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION