0

Hiểu về Java Stream API – Viết code ngắn gọn và dễ đọc hơn

Giới thiệu

Khi làm việc với Java, chúng ta thường xuyên phải xử lý danh sách dữ liệu như:

  • Danh sách nhân viên
  • Danh sách đơn hàng
  • Danh sách sản phẩm
  • Kết quả truy vấn từ database

Trước Java 8, các thao tác như lọc (filter), sắp xếp (sort), chuyển đổi dữ liệu (map) hay tính toán tổng hợp thường được thực hiện bằng các vòng lặp for truyền thống. Mặc dù hiệu quả, cách làm này đôi khi tạo ra nhiều đoạn code dài và khó đọc.

Để giải quyết vấn đề đó, Java 8 đã giới thiệu Stream API, cho phép lập trình viên xử lý tập dữ liệu theo phong cách khai báo (declarative programming) tương tự như SQL hoặc JavaScript Array Methods.

Stream API là gì?

Stream là một chuỗi các phần tử hỗ trợ thực hiện các phép toán trên dữ liệu theo dạng pipeline.

Thay vì mô tả từng bước xử lý dữ liệu bằng vòng lặp, lập trình viên chỉ cần mô tả kết quả mong muốn.

Ví dụ:

Giả sử có danh sách số:

List<Integer> numbers =
    Arrays.asList(1, 2, 3, 4, 5);

Muốn lấy các số chẵn:

List<Integer> evenNumbers =
    numbers.stream()
           .filter(n -> n % 2 == 0)
           .toList();

Kết quả:

[2, 4]

Code ngắn gọn và thể hiện rõ mục đích xử lý.

Tạo Stream

Có nhiều cách tạo Stream trong Java.

Từ Collection:

List<String> names =
    List.of("An", "Bình", "Cường");

Stream<String> stream =
    names.stream();

Từ mảng:

String[] names =
    {"An", "Bình", "Cường"};

Stream<String> stream =
    Arrays.stream(names);

Từ Stream.of():

Stream<String> stream =
    Stream.of("Java", "Spring", "Docker");

Sử dụng filter() để lọc dữ liệu

filter() là một trong những hàm được sử dụng nhiều nhất.

Ví dụ lấy nhân viên trên 30 tuổi:

List<Employee> employees =
    getEmployees();

List<Employee> result =
    employees.stream()
             .filter(emp -> emp.getAge() > 30)
             .toList();

Ưu điểm:

  • Code dễ đọc
  • Không cần tạo danh sách tạm
  • Tránh nhiều vòng lặp lồng nhau

Sử dụng map() để chuyển đổi dữ liệu

Trong thực tế, đôi khi chúng ta không cần toàn bộ object mà chỉ cần một số trường dữ liệu.

Ví dụ lấy danh sách tên nhân viên:

List<String> names =
    employees.stream()
             .map(Employee::getName)
             .toList();

Nếu dùng vòng lặp truyền thống:

List<String> names =
    new ArrayList<>();

for(Employee employee : employees){
    names.add(employee.getName());
}

Stream giúp code ngắn gọn và dễ hiểu hơn đáng kể.

Sử dụng sorted() để sắp xếp

Ví dụ sắp xếp lương giảm dần:

List<Employee> result =
    employees.stream()
             .sorted(
                 Comparator.comparing(
                     Employee::getSalary
                 ).reversed()
             )
             .toList();

Kỹ thuật này thường được dùng trong:

  • Dashboard
  • Báo cáo
  • API trả về danh sách dữ liệu

Sử dụng collect() để tổng hợp dữ liệu

Một trong những sức mạnh lớn nhất của Stream API là khả năng tổng hợp dữ liệu.

Ví dụ nhóm nhân viên theo phòng ban:

Map<String, List<Employee>> result =
    employees.stream()
             .collect(
                 Collectors.groupingBy(
                     Employee::getDepartment
                 )
             );

Kết quả:

IT -> [Employee1, Employee2]

HR -> [Employee3, Employee4]

Đây là thao tác rất phổ biến trong các hệ thống quản lý doanh nghiệp.

Tính toán với Stream

Ví dụ tính tổng:

int total =
    numbers.stream()
           .mapToInt(Integer::intValue)
           .sum();

Tìm giá trị lớn nhất:

Optional<Integer> max =
    numbers.stream()
           .max(Integer::compareTo);

Tính trung bình:

double average =
    numbers.stream()
           .mapToInt(Integer::intValue)
           .average()
           .orElse(0);

Các thao tác này giúp giảm đáng kể lượng code phải viết.

Stream Pipeline hoạt động như thế nào?

Một Stream thường gồm ba phần:

Nguồn dữ liệu

employees.stream()

Các thao tác trung gian

.filter(...)
.map(...)
.sorted(...)

Thao tác kết thúc

.toList()
.collect(...)
.count()
.sum()

Khi chưa có thao tác kết thúc, Stream sẽ chưa thực hiện xử lý dữ liệu.

Đây được gọi là Lazy Evaluation (đánh giá lười).

Cơ chế này giúp tối ưu hiệu năng và giảm các phép tính không cần thiết.

Khi nào không nên dùng Stream?

Mặc dù Stream rất mạnh, không phải lúc nào cũng là lựa chọn tốt nhất.

Ví dụ:

  • Logic xử lý quá phức tạp
  • Cần debug từng bước
  • Có nhiều điều kiện rẽ nhánh
  • Cần thao tác thay đổi trạng thái liên tục

Trong những trường hợp này, vòng lặp for truyền thống đôi khi lại dễ hiểu hơn.

Nguyên tắc quan trọng là ưu tiên khả năng đọc hiểu của code thay vì cố gắng sử dụng Stream ở mọi nơi.

Parallel Stream

Java còn hỗ trợ xử lý song song thông qua Parallel Stream.

Ví dụ:

employees.parallelStream()
         .filter(emp ->
             emp.getSalary() > 1000)
         .toList();

Parallel Stream tận dụng nhiều CPU Core để xử lý dữ liệu.

Tuy nhiên không phải lúc nào cũng nhanh hơn.

Đối với tập dữ liệu nhỏ, chi phí tạo luồng có thể còn lớn hơn lợi ích mang lại.

Vì vậy nên benchmark trước khi áp dụng trong môi trường thực tế.

Kết luận

Java Stream API là một trong những tính năng quan trọng nhất được giới thiệu từ Java 8. Nó giúp lập trình viên viết code ngắn gọn hơn, dễ đọc hơn và tập trung vào mục tiêu xử lý dữ liệu thay vì các chi tiết kỹ thuật của vòng lặp.

Các phương thức như filter(), map(), sorted()collect() xuất hiện rất thường xuyên trong các dự án Java hiện đại, đặc biệt là các ứng dụng sử dụng Spring Boot. Việc thành thạo Stream API không chỉ giúp tăng năng suất lập trình mà còn giúp mã nguồn trở nên chuyên nghiệp và dễ bảo trì hơn trong các dự án lớn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí