Chuyển tới nội dung chính

🏭 Dây Chuyền Sản Xuất - Luồng (Stream API)

Chúng ta đã có những "kho hàng" (Collections) được tổ chức rất tốt. Nhưng khi cần xử lý một lượng lớn "nguyên liệu" trong kho, ví dụ như lọc ra tất cả đơn hàng có món Latte, hay tính tổng giá của chúng, việc xử lý thủ công bằng vòng lặp for đôi khi khá dài dòng. Java 8 đã giới thiệu một "dây chuyền sản xuất" tự động cho việc này: Stream API.

🎯 Món Ăn Hôm Nay

Hôm nay, chúng ta sẽ học về Stream API. Hãy tưởng tượng nó như một dây chuyền sản xuất cà phê hiện đại. Bạn đưa một lô hạt cà phê (một Collection) vào đầu dây chuyền, nó sẽ tự động đi qua các trạm xử lý (lọc hạt xấu, rang, xay) và cho ra thành phẩm ở cuối dây chuyền.

Cách làm này giúp xử lý dữ liệu theo một luồng rất mạch lạc, dễ đọc và mạnh mẽ.

☕ Stream Là Gì?

Stream = Một dây chuyền xử lý dữ liệu.

Nó không phải là nơi chứa dữ liệu, mà là một chuỗi các hành động tác động lên dữ liệu từ một nguồn nào đó (ví dụ: một ArrayList).

Một dây chuyền Stream thường có 3 phần:

  1. Nguồn (Source): Nơi cung cấp "nguyên liệu", thường là một Collection (List, Set...).
  2. Các trạm xử lý trung gian (Intermediate Operations): Các bước biến đổi hoặc lọc dữ liệu trên dây chuyền. Ví dụ: filter() (lọc), map() (biến đổi), sorted() (sắp xếp). Các trạm này chỉ "lên kế hoạch" chứ chưa thực sự chạy.
  3. Trạm cuối (Terminal Operation): Hành động cuối cùng để cho ra kết quả và khởi động toàn bộ dây chuyền. Ví dụ: collect() (thu thành phẩm vào một Collection mới), forEach() (làm gì đó với từng thành phẩm), sum() (tính tổng).

👨‍🍳 Câu Chuyện Trong Quán

Quản lý yêu cầu Barista: "Hãy tìm tất cả các đơn hàng 'Latte' trong danh sách 1000 đơn hàng hôm nay, sau đó lấy tên khách hàng của các đơn đó và in ra theo thứ tự ABC."

Barista kiểu cũ (dùng for loop):

  1. Tạo một ArrayList rỗng để chứa các đơn Latte.
  2. Dùng for duyệt qua 1000 đơn hàng, nếu là Latte thì thêm vào list mới.
  3. Tạo một ArrayList rỗng khác để chứa tên khách.
  4. Dùng for duyệt qua list Latte, lấy tên khách cho vào list tên.
  5. Dùng Collections.sort() để sắp xếp list tên.
  6. Dùng for để in ra list tên đã sắp xếp. => Rất nhiều bước, code dài và phức tạp.

Barista hiện đại (dùng Stream API): Anh ta chỉ cần viết một "sơ đồ dây chuyền": danhSachDonHang.stream() -> lọc(là_Latte) -> biếnđổi(lấy_tên_khách) -> sắp_xếp() -> in_ra_màn_hình() => Code cực kỳ gọn gàng, rõ ràng và thể hiện đúng luồng xử lý.

📝 Công Thức "Vận Hành Dây Chuyền" (Code)

Giả sử chúng ta có một danh sách các đơn hàng:

List<String> orders = List.of("Anna - Latte", "Bob - Espresso", "Charlie - Latte");

Ví Dụ 1: Lọc Tất Cả Đơn Hàng Latte

import java.util.List;
import java.util.stream.Collectors;

// ...
List<String> latteOrders = orders.stream() // 1. Đưa đơn hàng lên dây chuyền
.filter(order -> order.contains("Latte")) // 2. Trạm lọc: chỉ giữ lại đơn có chữ "Latte"
.collect(Collectors.toList()); // 3. Trạm cuối: thu gom thành phẩm vào một List mới

System.out.println(latteOrders); // [Anna - Latte, Charlie - Latte]

Ví Dụ 2: Lấy Tên Khách Hàng Đặt Latte

import java.util.List;
import java.util.stream.Collectors;

// ...
List<String> customerNames = orders.stream()
.filter(order -> order.contains("Latte")) // Lọc đơn Latte
.map(order -> order.split(" - ")[0]) // Trạm biến đổi: từ "Anna - Latte" -> "Anna"
.collect(Collectors.toList());

System.out.println(customerNames); // [Anna, Charlie]

Ví Dụ 3: Đếm số ly Latte

import java.util.List;

// ...
long latteCount = orders.stream()
.filter(order -> order.contains("Latte")) // Lọc đơn Latte
.count(); // Trạm cuối: đếm số lượng

System.out.println("Tong so ly Latte: " + latteCount); // 2

🔥 Thực Hành Trong Quán

Bài Tập 1: Tìm Đồ Uống Giá Cao

Cho một danh sách giá tiền các món uống: List<Integer> prices = List.of(50000, 45000, 60000, 30000);. Sử dụng Stream để đếm xem có bao nhiêu món giá trên 45000.

Bài Tập 2: In Hóa Đơn

Cho danh sách giá tiền ở trên, sử dụng Stream để tính tổng tiền của tất cả các món.

  • Gợi ý: Dùng mapToInt() để chuyển Stream<Integer> thành IntStream rồi dùng sum().

⚠️ Những Lỗi Barista Thường Gặp

Lỗi 1: Quên Trạm Cuối (Terminal Operation)

// ❌ SAI: Dây chuyền chỉ được lên kế hoạch, không bao giờ chạy!
orders.stream().filter(o -> o.contains("Latte"));
  • Nguyên nhân: Các trạm trung gian (filter, map...) chỉ là những bước thiết kế. Dây chuyền sẽ không khởi động nếu thiếu một lệnh ở trạm cuối (collect, count, forEach...).
  • 🔧 Cách sửa: Luôn kết thúc một chuỗi stream bằng một trạm cuối.

Lỗi 2: Cố Gắng Tái Sử Dụng Stream

Stream<String> orderStream = orders.stream();
orderStream.forEach(System.out::println); // Ok

// ❌ SAI: Gây ra lỗi IllegalStateException
long count = orderStream.count();
  • Nguyên nhân: Một stream giống như một dòng nước chảy qua, không thể tắm hai lần trên cùng một dòng sông. Sau khi trạm cuối được thực thi, stream đó đã "cạn" và không thể dùng lại.
  • 🔧 Cách sửa: Nếu cần thực hiện nhiều thao tác, hãy tạo stream mới từ collection ban đầu: orders.stream().

💡 Bí Quyết Của Barista

  1. Stream không làm thay đổi nguồn: Stream API không bao giờ sửa đổi collection gốc. Mọi kết quả đều được trả về trong một collection mới hoặc một giá trị mới. Kho hàng của bạn luôn an toàn.
  2. Ưu tiên sự dễ đọc: Sức mạnh của Stream là giúp diễn tả bạn muốn gì thay vì bạn làm nó như thế nào. Hãy viết các chuỗi stream thật rõ ràng, mạch lạc.
  3. Sử dụng Method Reference: Để code ngắn gọn hơn nữa, hãy dùng method reference khi có thể. Ví dụ, map(String::toUpperCase) thay cho map(s -> s.toUpperCase()).

🎓 Bạn Đã Học Được

  • ✅ Stream là một "dây chuyền" xử lý dữ liệu từ một nguồn (collection).
  • ✅ Cấu trúc của stream: Nguồn -> Các trạm trung gian (lọc, biến đổi) -> Trạm cuối (thu gom, đếm).
  • ✅ Stream giúp viết code xử lý collection ngắn gọn, dễ đọc và mạnh mẽ hơn vòng lặp for truyền thống.
  • ✅ Stream không làm thay đổi dữ liệu gốc và không thể tái sử dụng.

☕ Món Tiếp Theo

Dây chuyền sản xuất của chúng ta hoạt động rất tốt, nhưng nó cần được vận hành đúng giờ. Làm thế nào để quản lý giờ mở cửa, đóng cửa, hay ngày hết hạn của nguyên liệu?

👉 Ngày Giờ - Quản Lý Lịch Làm Việc


💡 Lời Khuyên Cuối: Ban đầu, Stream API có thể hơi khó quen, nhưng hãy kiên trì. Một khi đã thành thạo, nó sẽ trở thành một trong những công cụ mạnh mẽ và được yêu thích nhất của bạn trong Java.