Giới thiệu
Trong tập trước, chúng ta đã làm quen với Predicate<T>
, giao diện chức năng cho logic boolean khai báo. Giờ đây, chúng ta sẽ bước vào bộ biến đổi dữ liệu: Function<T, R>
.
Nếu Predicate
trả lời câu hỏi “Cái này có hợp lệ không?”, thì Function
trả lời “Làm thế nào tôi có thể biến đổi cái này thành thứ khác?”. Giao diện này là trung tâm của lập trình chức năng trong Java, hỗ trợ mapping dữ liệu, pipelines và các biến đổi kinh doanh.
Function là gì?
Định nghĩa rất đơn giản:
java
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// Phương thức kết hợp
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { ... }
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { ... }
// Tiện ích
static <T> Function<T, T> identity() { ... }
}
- Đầu vào: một đối tượng kiểu T.
- Đầu ra: một đối tượng kiểu R.
- Mục đích: biến đổi đầu vào thành đầu ra.
Tại sao nên sử dụng Function?
Trước Java 8, việc biến đổi thường yêu cầu logic inline gắn liền với vòng lặp hoặc gọi dịch vụ:
java
List<String> names = new ArrayList<>();
for (User user : users) {
names.add(user.getName().toUpperCase());
}
Với Function
, việc biến đổi trở nên đầu tiên và có thể kết hợp:
java
Function<User, String> toUpperName = user -> user.getName().toUpperCase();
List<String> names = users.stream()
.map(toUpperName)
.toList();
Giờ đây, logic biến đổi trở nên rõ ràng, có thể tái sử dụng và kiểm tra.
Ví dụ thực tế
- Biến đổi cơ bản: Chuyển Integer thành String:
java
Function<Integer, String> intToString = i -> "Số: " + i;
System.out.println(intToString.apply(10)); // Số: 10
- Mapping qua Collections
Với streams, map
làm việc trực tiếp với Function
:
java
List<String> names = List.of("Hob", "André", "Borba");
Function<String, Integer> nameLength = String::length;
List<Integer> lengths = names.stream()
.map(nameLength)
.toList();
System.out.println(lengths); // [3, 5, 5]
- Kết hợp các Function
Sử dụng andThen
hoặc compose
để tạo pipelines:
java
Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> pipeline = multiplyBy2.andThen(square);
System.out.println(pipeline.apply(3)); // (3 * 2) ^ 2 = 36
- andThen: áp dụng đầu tiên, sau đó là cái tiếp theo.
- compose: áp dụng theo thứ tự ngược lại.
- Sử dụng Identity
Đôi khi, chúng ta cần một hàm không thực hiện gì:
java
Function<String, String> identity = Function.identity();
System.out.println(identity.apply("Xin chào")); // Xin chào
Hữu ích trong các pipelines tổng quát khi bạn không muốn thực hiện biến đổi.
Mô hình thực tế
1. Mapping DTO
Chuyển đổi các đối tượng miền thành Data Transfer Objects (DTOs).
java
Function<User, UserDTO> toDTO = user -> new UserDTO(user.getId(), user.getName());
2. Pipelines trong xử lý dữ liệu
Liên kết nhiều biến đổi (ví dụ: hệ thống ETL, bộ xử lý stream).
Quy tắc cụ thể theo miền
Đại diện cho các phép tính hoặc chính sách một cách khai báo (giá -> giá * giảm giá).
Thực hành tốt nhất
- Đặt tên cho Functions theo biến đổi: Tên tốt truyền đạt ý định:
toDTO
,toUpperName
,calculateTax
. - Tận dụng Composition: Xây dựng pipelines sử dụng
andThen
vàcompose
thay vì lồng ghép lambdas. - Ưu tiên Functions thuần túy: Giữ cho
Function
không có tác dụng phụ, điều này đảm bảo tính dự đoán và khả năng kiểm tra.
Những cạm bẫy phổ biến
- Tác dụng phụ ẩn: Đừng ghi log, thay đổi trạng thái hoặc ném ngoại lệ không kiểm soát bên trong các function.
- Pipelines quá phức tạp: Quá nhiều function kết hợp có thể làm mờ độ dễ đọc. Chia thành các function có tên trung gian.
- Mismatches về kiểu: Nhớ rằng
Function
là tổng quát, các loại không khớp trong composition thường gây lỗi biên dịch.
Phân tích chức năng
Hãy nghĩ về Function
như một bộ biến đổi trong một dây chuyền sản xuất:
- Nó nhận nguyên liệu thô (T).
- Biến đổi nó thành sản phẩm hoàn chỉnh (R).
- Có thể được liên kết với các bộ biến đổi khác để xây dựng một pipeline.
Kết luận
Function
là xương sống của việc biến đổi dữ liệu trong lập trình chức năng Java. Bằng cách tách biệt logic biến đổi, bạn có thể xây dựng pipelines khai báo, quy tắc kinh doanh sạch và mapping có thể tái sử dụng.
Nó nâng cao Java từ các vòng lặp mệnh lệnh đến workflows chức năng, có thể kết hợp.
Điều gì tiếp theo
Trong tập tiếp theo, chúng ta sẽ khám phá Consumer, giao diện cho các hành động không có giá trị trả về. Nếu Predicate
xác thực và Function
biến đổi, Consumer
thực thi.
Hãy theo dõi, bộ công cụ FP chỉ mới bắt đầu! 🚀