12/08/2018, 15:39

Lambda Expressions

... Tiếp theo phần trước (https://viblo.asia/p/nested-classes-p3-inner-class-anonymous-classes-jvEla6Xz5kw) Một vấn đề với anonymous classes là nếu nó rất đơn giản, chỉ chứa đúng 1 method, thì cú pháp của anonymous class có thể nhìn dài dòng và không rõ ràng. Trong trường hợp này, bạn thường muốn ...

... Tiếp theo phần trước (https://viblo.asia/p/nested-classes-p3-inner-class-anonymous-classes-jvEla6Xz5kw) Một vấn đề với anonymous classes là nếu nó rất đơn giản, chỉ chứa đúng 1 method, thì cú pháp của anonymous class có thể nhìn dài dòng và không rõ ràng. Trong trường hợp này, bạn thường muốn đưa hành động bên trong đó như là một đối số của một method khác, chẳng hạn như hành động sẽ thực hiện khi click vào một button. Lambda expressions sẽ cho phép bạn làm điều đó, cho phép functionality trở thành method argument. Ở phần trước, Anonymous Classes đã giới thiệu cách làm thế nào để implement một class mà ko cần gán tên cho nó. Mặc dù nó thường ngắn gọn hơn tạo 1 class có tên, nhưng với những class chỉ có 1 method, ngay cả anonymous class cũng trở lên dài dòng và cồng kềnh. Lambda expressions sẽ giúp bạn thể hiện single-method classes một cách ngắn gọn hơn nữa. Giả sử có một class Person như dưới:

public class Person {
    public enum Sex {
        MALE, FEMALE
    }
    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;
    public int getAge() {
        // ...
    }
    public void printPerson() {
        // ...
    }
}

Giả sử có một danh sách member trong một ứng dụng mạng xã hội được lưu trữ ở trong một List<Person>. Phần này sẽ bắt đầu với một số cách tiếp cận cho trường hợp cụ thể, nó sẽ cải thiện dần với local, anonymouse class và cuối cùng là lambda expression.

Approach 1: Create Methods That Search for Members That Match One Characteristic

Một hướng tiếp cận đơn giản là tạo vài method, mỗi method sẽ tìm kiếm member theo một đặc điểm, chẳng hạn giới tính hoặc tuổi. Method bên dưới sẽ in ra những member có tuổi lớn hơn 1 giá trị nào đó:

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

Cách tiếp cận này có thể làm cho ứng dụng của bạn khó thay đổi, nó có khả năng sẽ không hoạt động khi có một sự update. Giả sử bạn upgrade ứng dụng của bạn và thay đổi cấu trúc của Person class, làm chúng chứa thêm những member variable khác, có thể sẽ phải tính toán tuổi với một data type khác hoặc một thuật toán nào đó khác. Bạn sẽ phải viết lại khá nhiều code để đáp ứng những thay đổi này. Thêm nữa là cách này có chứa những hạn chế, ví dụ nếu bạn muốn in ra những member có tuổi nhỏ hơn một giá trị nào đó ?

Approach 2: Create More Generalized Search Methods

Method dưới khái quát hơn so với method trên kia, nó sẽ in ra những member với 1 dải tuổi

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

Bạn sẽ làm gì nếu muốn in ra những member với giới tính nào đó, hoặc kết hợp giới tính và độ tuổi ? Làm gì khi quyết định thay đổi Person class và thêm những thuộc tính khác như trạng thái quan hệ hoặc vị trí địa lý? Mặc dù method này khái quát hơn só với method ở phần 1, việc tạo ra 1 method riêng biệt cho mỗi query có thể dẫn đến code của bạn cứng nhắc, khó thay đổi. Thay vào đó, bạn có thể tách những đoạn code xác định có thỏa mãn những tiêu chí của bạn hay không trong một class khác.

Approach 3: Specify Search Criteria Code in a Local Class

Method dưới sẽ in ra những member thỏa mãn tiêu chí search mà bạn đề ra:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

Method này kiểm tra mỗi person chứa trong roster, xem có đáp ứng được các tiêu chí không bằng cách gọi tester.test. Nếu method tester.test trả về true thì method printPerson sẽ được gọi. Để định nghĩa những tiêu chí search, bạn phải implement CheckPerson interface:

interface CheckPerson {
    boolean test(Person p);
}

Class phía dưới implement CheckPerson interface. Method test sẽ lọc các member là MALE và có độ tuổi từ 18 đến 25:

class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}

Để sử dụng class này, cần tạo một instance của nó và gọi đến printPersons method:

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

Mặc dù cách tiếp cận này mềm dẻo hơn, bạn không phải viết lại các method nếu có thay đổi trong cấu trúc của Persion, tuy nhiên bạn vẫn có thêm khá nhiều code: 1 interface mới và 1 local class cho mỗi lần search. Vì CheckPersonEligibleForSelectiveService implement một interface, bạn có thể sử dụng anonymous class thay vì local class, và bỏ qua việc khai báo class mới cho mỗi lần search.

Approach 4: Specify Search Criteria Code in an Anonymous Class

Sử dụng anonymous class thay cho phần 3:

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

Viết như này sẽ giảm được tổng số code vì bạn không phải tạo mới một class cho mỗi việc search. Tuy nhiên, cú phải của nó nhìn cồng kềnh khi CheckPersion interface chỉ chứa đúng 1 method. Trong trường hợp này, bạn có thể sử dụng lambda expression thay cho một anonymous class, như mô tả ở phía dưới.

Approach 5: Specify Search Criteria Code with a Lambda Expression

CheckPerson là một functional interface. Một functional interface là một interface chỉ chứa đúng một abstract method (Một functional interface có thể chứa một hoặc nhiều default method hoặc static method). Vì chỉ chứa một abstract method, bạn có thể bỏ qua tên của method khi implement nó. Để làm vậy, thay vì sử dụng anonymous class expression, có thể sử dụng lambda expression, được viết như bên dưới:

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

Bạn có thể sử dụng một số standard functional interface ở vị trí của CheckPersion interface, sẽ giúp giảm nhiều hơn nữa số lượng code, như phía dưới.

Approach 6: Use Standard Functional Interfaces with Lambda Expressions

Xem xét lại CheckPerson interface

interface CheckPerson {
    boolean test(Person p);
}

Đây là một interface rất đơn giản, nó là một functional interface vì nó chỉ chứa 1 abstract method. Method này có 1 parameter và return boolean. Method này rất đơn giản nên nó có thể chúng ta sẽ ko muốn phải lúc nào cũng define trong app của bạn. Vì vậy, JDK đã define sẵn 1 số standard functional interfaces, có thể tìm thấy trong package java.util.function. Ví dụ, bạn có thể sử dụng Predicate<T> interface ở vị trí của CheckPersion. Interface này chứa một method boolean test(T t):

interface Predicate<T> {
    boolean test(T t);
}

Viết lại method với interface này:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

Gọi method bên dưới sẽ lọc các member là MALE và có độ tuổi từ 18 đến 25:

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

Đây không phải là trường hợp duy nhất có thể sử dụng lambda expression. Ở phía dưới sẽ suggest 1 số trường hợp có thể sử dụng lambda expression.

Approach 7: Use Lambda Expressions Throughout Your Application

Xem xét lại method printPersonsWithPredicate:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

Thay vì gọi method printPersion, bạn có thể gọi một action khác để thực hiện với những Person thỏa mãn điều kiện tester. Bạn có thể gọi action này với lambda expression. Giả sử bạn muốn một lambda expression giống printPerson, có một argument (một object kiểu Person) và return void. Nhớ rằng để sử dụng lambda expression, bạn cần implement một functional interface. Trong trường hợp này, bạn cần một functional interface chứa một abstract method có một argument kiểu Person và return void. Consumer<T> interface chứa method void accept(T t), có đủ các điều kiện này. Ở dưới sẽ thay thế việc gọi p.printPerson() bằng Consumer<Person>:

public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}

Gọi method bên dưới sẽ lọc các member là MALE và có độ tuổi từ 18 đến 25:

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

Nếu bạn muốn làm nhiều hơn với profile hơn là chỉ in ra ? Giả sử bạn muốn validate thông tin hoặc lấy thông tin từ chúng, trong trường hợp này bạn cần một functional interface chứa một abstract method return một giá trị. Function<T,R> interface chứa method R apply(T t). Đoạn code dưới sẽ lấy data từ mapper, sau đó thực hiện các action thông qua block:

public static void processPersonsWithFunction(
    List<Person> roster,
    Predicate<Person> tester,
    Function<Person, String> mapper,
    Consumer<String> block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}

Gọi method bên dưới sẽ lọc các member là MALE và có độ tuổi từ 18 đến 25, in ra email:

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

Approach 8: Use Generics More Extensively

Xem xét lại method processPersonsWithFunction. Dưới đây là một generic version:

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

Để in ra email address của các member là MALE và có độ tuổi từ 18 đến 25, gọi đến processElements method như sau:

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

Bạn có thể thay thế một số action bằng một aggregate operation tương ứng như bên dưới.

Approach 9: Use Aggregate Operations That Accept Lambda Expressions as Parameters

roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

Bảng dưới sẽ map từng operation của mà method processElements thực hiện với aggregate operation tương ứng: Những operation filter, map hay forEarch là aggregate operations. Chúng thực hiện từ 1 stream, không trực tiếp từ một collection (đó là lý do method đầu tiên được gọi trong ví dụ này là stream). Một stream là 1 sequence của các elements. Không giống như collection, nó không phải là một data structure để lưu trữ các element. Thay vào đó, một stream mang các giá trị từ 1 nguồn, chẳng hạn như collection, xuyên qua một đường ống dẫn. Một ống dẫn là một sequence của các operations, trong ví dụ này là filter - map - forEach. Ngoài ra, aggregate operations cũng nhận lambda expression như là những parameter, cho phép bạn customize dễ dàng hơn.

Nguồn bài viết:

https://docs.oracle.com/javase/tutorial/java

0