Giới thiệu
Trong lập trình Java, việc xử lý dữ liệu bằng các stream đã trở nên phổ biến nhờ vào tính linh hoạt và khả năng tối ưu hóa. Một trong những khái niệm mới và thú vị trong Java là Gatherer, một dạng trừu tượng giống như collector cho phép bạn thực hiện các phép biến đổi phức tạp hơn trên các stream. Bài viết này sẽ giúp bạn hiểu rõ về Gatherer, cách sử dụng và những lưu ý khi triển khai nó.
1. Gatherer là gì?
Gatherer là một trừu tượng mới trong Java, cho phép bạn biến đổi một stream theo cách cần duy trì trạng thái hoặc xem xét nhiều phần tử cùng lúc, vượt ra ngoài khả năng của map
, filter
, và flatMap
. Hãy tưởng tượng rằng Gatherer giống như một phép toán trung gian tùy chỉnh, có sức mạnh tương đương với việc viết collector của riêng bạn, nhưng dành cho các phép toán trung gian thay vì các phép toán cuối.
Các ứng dụng của Gatherer
- Chuyển đổi trạng thái (như "nhóm các phần tử liên tiếp", "chia thành các cửa sổ", "chỉ phát hành các thay đổi").
- Khi các phép toán cơ bản như
map
,filter
, vàflatMap
không đủ. - Khi bạn cần các phép biến đổi có thể tái sử dụng và kết hợp mà không cần phải sử dụng vòng lặp mệnh lệnh.
2. Khi nào nên và không nên sử dụng Gatherer
Nên sử dụng Gatherer khi:
- Bạn cần thực hiện các phép biến đổi có trạng thái.
- Các phép toán cơ bản không đáp ứng đủ nhu cầu.
- Bạn muốn các phép biến đổi có thể tái sử dụng và kết hợp.
Không nên sử dụng Gatherer khi:
- Một phép toán đơn giản như
map
,filter
, hayflatMap
đã đủ. - Bạn chỉ cần một phép giảm cuối (như
collect
,reduce
) → thì nên sử dụng mộtCollector
. - Hiệu suất là điều quan trọng và bạn không cần hành vi trạng thái phức tạp → Gatherer có thể gây overhead về mặt trừu tượng.
3. Cách sử dụng Gatherer
API của Gatherer tập trung vào phương thức Gatherer.of(...)
. Một Gatherer định nghĩa cách xử lý các phần tử và phát hành không có, một, hoặc nhiều đầu ra cho mỗi đầu vào.
Một Gatherer
bao gồm:
- Initializer → tạo trạng thái có thể thay đổi (tùy chọn).
- Integrator → xử lý mỗi phần tử đầu vào cùng với trạng thái.
- Finisher → bước cuối cùng khi stream kết thúc (tùy chọn).
Cách định nghĩa Gatherer
java
static <T, R, S> Gatherer<T, S, R> of(
Supplier<S> initializer,
Integrator<T, S, R> integrator,
BiConsumer<S, Downstream<? super R>> finisher
)
4. Ví dụ cụ thể
Ví dụ 1: Xóa các phần tử trùng lặp liên tiếp
java
import java.util.stream.*;
public class GathererExample1 {
public static void main(String[] args) {
var result = Stream.of("A", "A", "B", "B", "C", "A", "A")
.gather(Gatherers.distinctAdjacent()) // gatherer tích hợp sẵn
.toList();
System.out.println(result); // [A, B, C, A]
}
}
Gatherers.distinctAdjacent()
là một gatherer tích hợp sẵn giúp xóa các phần tử trùng lặp liên tiếp.
Ví dụ 2: Chia thành các nhóm (Cửa sổ)
java
import java.util.stream.*;
import java.util.*;
public class GathererExample2 {
public static void main(String[] args) {
var result = Stream.iterate(1, n -> n + 1).limit(10)
.gather(Gatherers.windowFixed(3)) // cửa sổ kích thước 3
.toList();
System.out.println(result);
// [[1,2,3], [4,5,6], [7,8,9], [10]]
}
}
Gatherers.windowFixed(3)
nhóm các phần tử thành các danh sách có kích thước 3
.
Ví dụ 3: Gatherer tùy chỉnh — Phát hành chỉ giá trị tăng dần
java
import java.util.stream.*;
import java.util.function.*;
public class GathererExample3 {
public static void main(String[] args) {
var increasingGatherer = Gatherer.of(
() -> new int[]{Integer.MIN_VALUE}, // bộ giữ trạng thái
(state, elem, downstream) -> {
if (elem > state[0]) {
state[0] = elem;
downstream.push(elem); // chỉ phát hành nếu lớn hơn
}
return true; // tiếp tục
}
);
var result = Stream.of(1, 2, 2, 5, 3, 7, 6, 8)
.gather(increasingGatherer)
.toList();
System.out.println(result); // [1, 2, 5, 7, 8]
}
}
Ở đây, chúng ta đã viết một Gatherer tùy chỉnh chỉ phát hành khi chuỗi tăng dần.
5. Các Gatherers tích hợp sẵn trong Java
Java cung cấp một số gatherers sẵn có trong java.util.stream.Gatherers
:
distinctAdjacent()
→ xóa các phần tử trùng lặp liên tiếp.scanLeft(initial, op)
→ quét tích lũy (giống nhưreduce
nhưng giữ lại các giá trị trung gian).windowFixed(size)
→ nhóm các phần tử thành cửa sổ có kích thước cố định.windowSliding(size)
→ cửa sổ trượt.mapConcurrent(...)
→ ánh xạ đồng thời.
6. Tóm tắt
- Gatherer = Biến đổi trung gian tùy chỉnh cho các stream.
- Sử dụng khi bạn cần xử lý phần tử có trạng thái hoặc phức tạp.
- Cung cấp các gatherers tích hợp sẵn (
windowFixed
,distinctAdjacent
, v.v.). - Bạn cũng có thể định nghĩa gatherers tùy chỉnh cho các nhu cầu cụ thể trong miền của bạn.
Các lưu ý và mẹo khi sử dụng Gatherer
- Hãy chắc chắn rằng bạn hiểu rõ về trạng thái mà bạn đang duy trì, vì điều này có thể ảnh hưởng đến hiệu suất và cách thức hoạt động của stream.
- Kiểm tra hiệu suất của Gatherer trong các tình huống thực tế để đảm bảo rằng nó đáp ứng được yêu cầu của bạn.
- Tránh lạm dụng Gatherer cho các tác vụ đơn giản, vì nó có thể gây ra overhead không cần thiết.
Câu hỏi thường gặp (FAQ)
1. Gatherer có giống với Collector không?
Trả lời: Có, nhưng Gatherer chủ yếu được sử dụng cho các phép toán trung gian trong khi Collector được sử dụng cho các phép toán cuối.
2. Tôi có thể định nghĩa Gatherer tùy chỉnh không?
Trả lời: Có, bạn có thể định nghĩa Gatherer tùy chỉnh theo nhu cầu cụ thể của bạn.
3. Gatherer có hiệu suất tốt không?
Trả lời: Gatherer có thể tốt cho các biến đổi phức tạp nhưng cần được kiểm tra trong các tình huống cụ thể để đánh giá hiệu suất.
4. Có bao nhiêu loại Gatherers tích hợp sẵn trong Java?
Trả lời: Java cung cấp một số gatherers tích hợp sẵn như distinctAdjacent
, windowFixed
, và scanLeft
. Hãy khám phá chúng để biết thêm chi tiết.