Tổng hợp các Functional Interface trong Java 8

Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu các Functional Interface được giới thiệu trong Java 8, cũng như cách sử dụng chúng trong các trường hợp cụ thể.

Functional interface

Functional Interface là một interface chứa duy nhất một abstract method. Từ java 8 trở đi lambda expressions được sử dụng nhiều với functional interface để thể hiện một implement của nó.

Một functional interface có thể có nhiều default method nên các bạn đừng nhầm tưởng rằng functional interface lại có nhiều method thế kia nhé, nó chỉ có một abstract method duy nhất thôi!

Note: Tất cả các functional interface đều được khuyến khích đi kèm với annotation @FunctionalInterface, điều này sẽ giúp trình biện dịch có đủ thông tin để báo lỗi khi một functional interface không thoả các điều kiện (chỉ chứa 1 abstract method).

Các functional interface

Java 8 cung cấp sẵn một số Functional Interface để xây dựng các mô hình cơ bản nhất trong Java. Chúng ta sẽ tìm hiểu qua 1 số interface sau:

Function Interface

Function inteface là interface đơn giản với method nhận vào một tham số và trả về một giá trị.

public interface Function<T, R> {
    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

   // statics, methods 
}

Một trường hợp sử dụng phổ biến của Function inteface là trong map() method của Stream API, dùng để chuyển Stream<T> sang một Stream<R>.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Ví dụ chuyển từ Stream<String> sang String<Interger> tương tứng với độ dài của string tương ứng. 

Stream<String> strings = Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck");
Stream<Integer> sizes =  strings.map(String::length);

Primitive Function

Một vấn đề đặt ra là các kiểu dữ liệu nguyên thuỷ không thể sử dụng trong Generic Type. Vì thế chúng ta có một số biến thể của Function interface áp dụng cho các kiểu dữ liệu nguyên thuỷ: int, long, double etc.

  • IntFuction, LongFunction, DoubleFunction: Nhận tham số với kiểu dữ liệu tương ứng và trả về một giá trị có kiểu dữ liệu bất kỳ. 
  • ToIntFunction, ToLongFunction, toDoubleFunction: Nhận tham số có kiểu dữ liệu bất kỳ và trả về kiểu dữ liệu nguyên thuỷ tương ứng.

Ngoài các Primitive Function interface được nêu ở trên ra, chúng ta sẽ không được cung cấp thêm các Primitive interface cho các kiểu khác. Tuy nhiên chúng ta có thể tự triển khai chúng ta có như cầu cần thiết.

Ví dtriển khai thêm FloatFunction functional interface

@FunctionalInterface
public interface FloatFunction<R> {
   R apply(float value);
}

Hoặc một Functional interface nhận tham số kiểu short và trả về byte.

@FunctionalInterface
public interface ShortToByteFunction {
    byte applyAsByte(short s);
}

Bây giờ, chúng ta sẽ viết một function chuyển một mảng short sang mảng byte theo quy tắc được định nghĩa bởi một triển khai của ShortToByteFunction.

public class Test {
    public static void main(String[] agrs) {
        short[] array = {(short) 1, (short) 2, (short) 3};
        byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));
        for (byte b : transformedArray) {
            System.out.print(b + " ");
        }
    }

    public static byte[] transformArray(short[] array, ShortToByteFunction function) {
        byte[] transformedArray = new byte[array.length];
        for (int i = 0; i < array.length; i++) {
            transformedArray[i] = function.applyAsByte(array[i]);
        }
        return transformedArray;
    }
}

BiFunction

BiFunction định nghĩa biểu thức với 2 tham số đầu vào và trả trả về một kết quả.

@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);
    // default, static method other
}

Ngoài ra chúng ta cũng có các biến thế của BiFunction cho phép thao tác với các kiểu kiểu dữ liệu nguyên thuỷ như ToDoubleBiFunction, toIntBiFunction, toLongBiFunction nhận vào 2 tham số generic type và trả về kiểu dữ liệu tương tứng double, int, long.

Một ví dụ điển hình sử dụng BiFunction trong Stream API là reduce() method. 

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

Stream.reduce() cho phép chúng ta tạo ra một kết quả duy nhất từ các phần tử của Stream bằng cách áp dụng các phép tính lặp đi lặp lại trên các phần tử của nó.

Ví dụ tính tổng các phần tử trong List<Integer sử dụng Parallel Stream.

List<Integer> ages = Arrays.asList(25, 30, 45, 28, 32);
int computedAges = ages.parallelStream().reduce(0, (a, b) -> a + b, Integer::sum);

Supplier

Supplier khác với Function,  không nhận bất kỳ một tham số nào, và trả về giá trị generic type. Thông thường, Supplier được sử dụng cho các lazy generation (khi nào cần mới lấy ra sử dụng).

Ví dụ viết 1 function tính diện tích hình vuông, điều đặc biệt là nó sẽ không nhận giá trị của cạnh hình vuông như một tham số mà sẽ do 1 Supplier cung cấp.

public double squareLazy(Supplier<Double> lazyValue) {
    return Math.pow(lazyValue.get(), 2);
}

Điều này, cho phép chúng ta tạo ra một tham số lazy-generation, chỉ được khởi tạo khi thực hiện lời gọi hàm. Điều này sẽ rất có ích khi tham số là một đối tượng tốn nhiều thời gian để khởi tạo.

Supplier<Double> lazyValue = () -> {
    Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
    return 9d;
};
 
Double valueSquared = squareLazy(lazyValue);

Consumer

Trái ngược với Supplier, Consumer sẽ nhận một tham số và không trả về bất kỳ một giá trị nào, thông thường nó được sử dụng để thao tác trực tiếp trên các đối tượng. 

Ví dụ Consumer được sử dụng trong Stream forEach() method dùng để duyệt các phần tử của Stream.

List<String> names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));

Ngoài ra, chúng ta còn có BiConsumer hoạt động tương tự Consumer, chỉ khác là BiConsumer nhận vào 2 tham số.

Ví dụ forEach() trong HashMap sử dụng BiConsumer cho phép duyệt các phần tử trong Map theo từng cặp key-value

Map<String, Integer> ages = new HashMap<>();
ages.put("John", 25);
ages.put("Freddy", 24);
ages.put("Samuel", 30);
 
ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

Predicate

Predicate được sử dụng trong các toán tử logic, nhận vào một tham số và trả về giá trị boolean. Để ý kỹ thì chúng ta có thể thấy Predicate là một biến thể của Function với mục đích sử dụng rõ ràng hơn.

List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
 
List<String> namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

Ở đoạn code trên, chúng ta đang lọc ra các phần tử bắt đầu với “A“, trong đó filter() method nhận vào một Predicate dùng để kiểm tra các phần tử theo điều kiện cụ thể.

Tương tự, Predicate cũng có các biến thể của Predicate cho các kiểu dữ liệu nguyên thuỷ: IntPredicateDoublePredicate và LongPredicate 

Operator

UnaryOperator interface là một biến thể của Function, nhận vào một tham số và trả về một giá trị cùng kiểu dữ liệu dữ liệu. Trong Collection, UnaryOperator được sử dụng trong thay đổi tất cả các phần tử với replaceAll() method.

List<String> names = Arrays.asList("bob", "josh", "megan");
 
names.replaceAll(name -> name.toUpperCase());

Một biến thể khác trong nhóm OperatorBinaryOperator, nhận vào 2 tham số và trả về 1 tham số đều có cùng kiểu dữ liệu.

Tóm lược

Không phải tất cả các functional interface đều xuất hiện từ Java 8, có rất nhiều interface xuất hiện từ các phiên bản trước đều tuân thủ theo các nguyên tắc của functional interface ví dụ như RunnableCallable interface. Trong Java 8 chúng chỉ đơn giản là thêm @FunctionalInterface annotation

Nguồn tham khảo

https://www.baeldung.com/java-8-functional-interfaces

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x