Biến đổi stream qua các operation trung gian

Biến đổi stream qua các operation trung gian

Java Stream API

·

4 min read

Stream chuyên dùng để xử lý dữ liệu, và stream thực hiện được điều đó nhờ các operation. Mỗi loại operation sẽ có cách biến đổi khác nhau, và lập trình viên sẽ "lắp ghép" 😂 chúng lại phù hợp để cho ra kết quả mong muốn.

Và operation có 2 loại là intermediate operation (trung gian) và terminal operation (kết thúc). Trong bài này hãy cùng mình tìm hiểu về các loại operation trung gian trước nhé.

1. Chút lý thuyết về intermediate operation?

Operation là một hành động được áp dụng lên stream, mỗi loại operation sẽ biến đổi (transform) stream theo một cách nhất định. Loại operation trung gian gồm những operation nhận vào một stream và trả về stream khác (đã biến đổi).

Các operation trung gian có thể nối lại với nhau, output của operation trước sẽ là input cho operation tiếp theo.

List<Integer> nums = List.of(2, 3, 5, 7);
nums.stream() // Tạo ra Stream<Integer>
    .mapToInt(Integer::valueOf) // Operation trung gian
    .sum(); // Terminal operation

Các intermediate operation có dạng là các method của Stream<T> (hoặc primitive stream). Các method này sẽ return một stream khác. Do đó, khi viết code có thể chain các method lại với nhau, đúng theo nguyên tắc như trên.

Những operation method này sẽ có param là functional interface, để thực hiện phép biến đổi tương ứng. Ví dụ map() biến element thành giá trị khác, nên nó dùng Function<T, U>, còn filter() cần quyết định xem có giữ lại element không, thì nó dùng Predicate<T>.

2. Top 10 operation trung gian cơ bản

Hãy bắt đầu với 5 loại cơ bản và được dùng thường xuyên qua ví dụ sau (ví dụ chưa chạy do tính lazy của intermediate operation).

List<Integer> nums = List.of(2, 3, 5, 7);

// Xử lý element gì đó và return chính nó
// Như forEach() mà nó là operation trung gian
nums.stream().peek(System.out::println); // Consumer<T>

// Biến đổi 1 element thành 1 element khác
nums.stream().map(e -> e * e); // Function<T, U>

// Lọc ra và chỉ giữ lại các element thỏa điều kiện
nums.stream().filter(e -> e > 10); // Predicate<T>

// Giới hạn max số lượng phần tử
// Thường dùng cho stream vô hạn để giới hạn bớt
nums.stream().limit(3);

// Bỏ qua N phần tử đầu tiên
nums.stream().skip(2);

Và tiếp theo là 5 loại khác, ít được sử dụng hơn (một tí hoi).

// Loại bỏ các element trùng nhau (chỉ giữ lại element phân biệt)
nums.stream().distinct();

// Sắp xếp các element
nums.stream().sorted();

// Loại bỏ ràng buộc ORDERED trên stream
// Dùng tối ưu stream hơn trong trường hợp cụ thể
nums.stream().unordered();

// Convert thành stream được xử lý song song
nums.stream().parallel();

// Convert thành stream xử lý tuần tự
nums.stream().sequential();

Đến đây bạn đã nắm được 10 operation trung gian phổ biến nhất rồi. Tất nhiên trong Stream API còn định nghĩa thêm nhiều loại khác nữa, nhưng ít dùng hơn, mọi người có thể google thêm nhé.

3. Bonus vài loại nâng cao hơn nữa

Java 9 bổ sung thêm 2 operation trung gian sau:

  • takeWhile() tiếp tục xử lý element hiện tại khi điều kiện đúng, nếu sai thì dừng lại (toàn bộ element còn lại sẽ bị bỏ qua)
  • dropWhile() tiếp tục bỏ qua element khi điều kiện đúng, nếu sai thì toàn bộ element còn lại sẽ được giữ lại (mà không cần kiểm tra điều kiện)
var nums = List.of(2, 3, 5, 7, 11, 13, 1, 2, 3, 4);

// Khi nào còn nhỏ hơn 10 thì tiếp tục lấy
// Kết quả lấy được là 2, 3, 5, 7 (phần sau bỏ hết)
nums.stream().takeWhile(num -> num < 10);

// Khi nào còn nhỏ hơn 10 thì vẫn bị bỏ qua
// Kết quả lấy được là 11, 13, 1, 2, 3, 4 (lấy hết phần sau)
nums.stream().dropWhile(num -> num < 10);

Một operation nữa mà mình thấy "ối giời ơi" nhất là flatMap(). Có thể do ít dùng thực tế nên mình cứ học rồi lại quên miết. Khác với map() là 1-1, flatMap() là dạng 1-N, nhận một phần tử và trả về một stream các phần tử.

// Với phần tử X thì lặp lại X lần
List<Integer> nums = List.of(1, 3, 5);
nums.stream()
    .flatMap(x -> Stream.iterate(x, i -> i).limit(x)) // Từ 1 tới x
    .toList(); // [1, 3, 3, 3, 5, 5, 5]

// Trường hợp khác là làm phẳng ma trận
List<List<Integer>> matrix = List.of(
    List.of(1, 2, 3),
    List.of(4, 5, 6));
matrix.stream()
    .flatMap(x -> x.stream()) // Lúc này x là một List<Integer>
    .toList(); // [1, 2, 3, 4, 5, 6]

Code trên minh họa hai trường hợp mình thấy rất hay sử dụng của flatMap(). Hơi khó hiểu tí, mọi người xem kĩ phần này nhé.


Okay vậy thôi, bài viết hôm nay tới đây xin được tạm dừng. Bye bye 😘 và hẹn mọi người ở các bài viết tiếp theo nhé.