Stream sau khi qua nhiều lần biến đổi với intermediate operation sẽ được tổng hợp lại thành kết quả cuối cùng với terminal operation. Đây là kết quả mà lập trình viên mong muốn đạt được, đã được tổng hợp lại và việc thực hiện stream kết thúc.
1. Vài tí lý thuyết về terminal operation
Ở bài trước mình đã nói qua về intermediate operation, chúng sẽ nhận vào stream input và trả về stream khác. Stream được operation trước trả về sẽ làm input cho operation tiếp theo.
Với terminal operation hơi khác tí, nó cũng nhận vào stream nhưng không trả về stream khác, thay vào đó sẽ trả về kết quả (mà lập trình viên mong muốn đạt được khi xử lý stream).
Kết quả ở đây có thể là một value với min()
, count()
,..., một mảng, collection collect()
, toArray()
hoặc là một công việc nào đó được thực hiện như forEach()
. Và sau đó thì stream kết thúc.
2. Các terminal operation thông dụng
2.1. Các loại cơ bản
Cho dễ thì chỉ cần xem qua ví dụ sau là hiểu ngay. Lưu ý là các ví dụ chỉ có terminal operation thôi, mình không viết operation trung gian cho đơn giản.
List<Integer> primes = List.of(2, 3, 5, 7);
// Áp dụng xử lý trên mọi element
primes.stream().forEach(System.out::println); // Consumer<T>
primes.stream().forEachOrdered(System.out::println);
// Đếm số lượng element
int count = primes.stream().count();
// Tìm min, max cần một Comparator<T> để so sánh
int min = primes.stream().min((a, b) -> a - b);
int max = primes.stream().max((a, b) -> b - a);
// Tìm element đầu tiên
primes.stream().findFirst();
primes.stream().findAny(); // Cho parallel
// Xét stream và trả về boolean
primes.stream().anyMatch(e -> e > 10); // Bất kì số nào lớn hơn 10?
primes.stream().allMatch(e -> e > 0); // Mọi số đều dương?
primes.stream().noneMatch(e -> e == 0); // Không có số 0 nào
2.2. reduce()
Đây là operation khá đặc biệt và "không dễ hiểu" như những operation trên.
Reduce là "giảm" một dãy element xuống dần dần chỉ còn lại một giá trị duy nhất.
reduce()
sẽ nhận vào một init value và một BinaryOperation<T>
. Phần sau gồm 2 tham số accumulator và current. Accumulator là giá trị tổng hợp được ở bước trước đó, còn current là element đang xét hiện tại. Return về giá trị tổng hợp ở bước hiện tại.
int sum = Stream.of(2, 3, 5, 7).reduce(0, (acc, curr) -> acc + curr);
// Ban đầu sum sẽ được gán init value
// Bắt đầu với element đầu tiên, sum mới sẽ là sum cũ + element
// Tiếp tục element tiếp theo, sum mới sẽ là sum cũ + element
// Cứ tiếp tục như thế tới hết stream, thu được sum là 17
Cứ tiếp tục như vậy tới hết element, thì từ một stream sẽ chỉ còn lại một value duy nhất. Hoạt động này được dựa trên Fold higher order function.
Lưu ý, nên luôn có init value, nếu không thì khi stream rỗng sẽ trả về Optional<T>
thay vì số mong muốn.
2.3. toArray()
và collect()
Chuyển đổi một stream thành array thì dùng toArray()
như sau.
List.of(2, 3, 5, 7).stream().toArray(Integer[]::new);
Còn collect()
thì sẽ gom stream lại thành một collection nào đó, hoặc thực hiện gom nhóm theo cách nào đó.
// Chuyển stream thành collection
List.of(2, 3, 5, 7).stream().collect(Collectors.toList());
// Ngoài ra Collectors còn có các method đặc biệt khác
// Để thực hiện collect theo những cách đặc biệt 😂
Trường hợp chuyển stream thành list mà dùng collect()
thì hơi dài dòng, nên Java 16 bổ sung thêm toList()
terminal operation cho nhanh.
2.4. Riêng của primitive stream
Các primitive stream có thêm các terminal operation khác mà Stream<T>
không có.
// Các tính toán cơ bản
IntStream.of(1, 2, 3).sum();
IntStream.of(1, 2, 3).average();
// Tổng hợp cùng lúc count, sum, min, max, average
IntSummaryStatistics total = IntStream.of(1, 2, 3).summaryStatistics();
total.getAverage(); // getSum(), getMin(),...
3. Reduction và mutable reduction
Reduction operation về cơ bản là các terminal operation tổng hợp stream thành kết quả. Không phải mọi terminal operation đều là reduction operation. Có 2 loại:
- Tổng hợp thành 1 giá trị duy nhất như
min()
,max()
,count()
,...,summaryStatistics()
- Tổng hợp thành một nhóm giá trị (mutable reduction) như
collect()
,toArray()
,toList()
Thường mọi phương pháp reduction đều thay thế được bởi reduce()
. Ví dụ min()
, count()
, collect()
,... đều có thể viết lại dựa trên reduce()
hết. Ví dụ nhé.
List<Integer> nums = List.of(2, 3, 5, 7);
long count1 = nums.stream().count();
long count2 = nums.stream().reduce(0, (acc, curr) -> acc + 1);
Với mutable reduction như collect()
, sẽ hiệu quả hơn nếu dùng collect()
thay cho reduce()
. Vì reduce()
chỉ dùng cho kết quả có tính immutable, vì nó thay thế kết quả accumulator
trong từng bước với kết quả trước đó, nghĩa là tạo đối tượng accumulator
qua từng bước (tưởng tượng như tính immutable của String).
Trong khi đó, collect()
chỉ tạo đối tượng accumulator
một lần, và là mutable nên khi tổng hợp qua từng bước sẽ không tạo đối tượng mới nữa, chỉ đơn giản là update lại vào accumulator
cũ thôi.
Như trên mình đã điểm qua kha khá terminal operation cơ bản, cũng giúp các bạn có cái nhìn tổng quan về reduction rồi. Hãy tiếp tục đón chờ mình ở những bài viết tiếp theo trong chủ đề Stream API này nhé. Thank you!