Java – Closure

Hôm nay chúng ta sẽ cùng nhau tìm hiểu về Closure trong Java. Ngoài ra chúng ta cũng sẽ tìm hiểu về Java 8 Lamba Expression

Closure là một hàm / hành vi sử dụng biến được định nghĩa nằm ngoài phạm vi của hàm hay phương thức.

Giả sử chúng ta muốn tạo Thread một cách dễ dàng để in ra console như sau:

int answer = 14;
Thread t = new Thread(
  () -> System.out.println("The answer is: " + answer)
);

Java 8 Lamba Expression là gì

int answer = 42;
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("The answer is: " + answer);
    }
});

Từ Java 8, ví dụ bên trên có thể được viết lại sử dụng Java 8 Lamba Expression

Giờ đây thì tất cả chúng ta đều biết rằng Java 8 Lamba không chỉ giúp  giảm tính rườm rà trong code, chúng còn được biết đến với nhiều tính năng mới khác nhau. Hơn nữa, có sự khác nhau giữa phần thực thi của anonymous class và Lamba Expression.

Nhưng điểm chính nổi bật ở đây là cân nhắc xem làm thế nào chúng tương tác với phạm vi đóng, chúng ta có thể nghĩ chúng chỉ như là một cách nhỏ gọn để tạo class interface anonymous, như là Runnable , Callable , Function , Predicate… Trong thực tế tương tác giữa một Lamba Expression và phạm vi đóng  của nó là tương đối giống nhau (ví dụ khác nhau về mặt ngữ nghĩa của từ khóa this)

Giới hạn Java 8 Lamba

Lamba Expression (được biết như là anonymous class) trong Java chỉ có thể truy cập đến các biến final (giá trị không thay đổi) của phạm vi đóng

Ví dụ hãy cùng xem ví dụ sau đây

void fn() {
    int myVar = 42;
    Supplier<Integer> lambdaFun = () -> myVar; // error
    myVar++;
    System.out.println(lambdaFun.get());
}

Nó sẽ lỗi khi biên dịch bởi vì chúng ta tăng giá trị của myVar

Javascript và các hàm

Các hàm và Lamba Expression trong Javascript sử dụng khái niệm của Closure

Một Closure có thể là một kiểu object đặc biệt kết hợp 2 điều sau: Một hàm và môi trường nơi hàm được tạo. Môi trường bao gồm bất kỳ biến local nào trong phạm vi tại thời điểm Closure được tạo.

Trong Javascript, ví dụ bên trên hoạt động tốt

function fn() { // Phạm vi đóng
    var myVar = 42;
    var lambdaFun = () => myVar;
    myVar++;
    console.log(lambdaFun()); // in ra 43
}

Hàm Lamba trong ví dụ sử dụng giá trị myVar sau khi thay đổi.

Trong Javascript, hàm mới được tạo trỏ đến phạm vi đóng nơi nó được tạo. Cơ chế cơ bản này cho phép tạo các Closure lưu vị trí lưu trữ của các biến độc lập, do đó chúng có thể được chỉnh sửa bởi chính hàm của nó

Java Clouse

Java chỉ lưu giá trị của các biến độc lập và cho phép chúng được sử dụng bên trong Lamba Expression. Thậm chí nếu chúng ta tăng giá trị của myVar, hàm của Lamba vẫn trả về 42. Trình biên dịch sẽ tránh những kịch bản không mạch lạc rõ ràng, giới hạn các kiểu biến có thể được sử dụng bên trong Lamba Expression (và anonymous class) để nó chỉ là cuối cùng và có hiệu quả cuối cùng.

Mặc dù giới hạn này nhưng chúng ta có thể tuyên bố rằng Java 8 thực thi Closure. Trong thực tế, các Closure, thuộc về mặt lý thuyết nhiều hơn, chỉ nắm giữ các giá trị của các biến độc lập. Trong các ngôn ngữ functional thuần túy, đây là điều duy nhất được cho phép, giúp cho thuộc tính minh bạch tham chiếu.

Sau đó, một vài ngôn ngữ functional như là Javascript, đã giới thiệu khả năng nắm giữ vị trí lưu trữ của các biến độc lập, điều này cho phép khả năng bị Side Effect.

Như vậy chúng ta có thể coi rằng Closure của Javascipt có thể làm nhiều hơn, nhưng vấn đề Side Effect đó thực sự giúp Javascript như thế nào? Và chúng có thật sự quan trọng không?

Side Effect và Javascript

Để hiểu rõ hơn về khái niệm Closure, hãy cùng xem đoạn code Javascript sau

function createCounter(initValue) { // phạm vi đóng
    var count = initValue;
    var map = new Map();
    map.set('val', () => count);
    map.set('inc', () => count++);
    return map;
}
v = createCounter(42);
v.get('val')(); // returns 42
v.get('inc')(); // returns 42
v.get('val')(); // returns 43

Mỗi lần hàm createCounter được gọi, nó tạo một map với 2 hàm Lamba mới, mà tương ứng trả về giá trị và tăng giá trị của biến được định nghĩa trong phạm vi đóng.

Nói một cách khác là hàm có Side Effect đã làm thay đổi giá trị của biến và ảnh hưởng đến kết quả của các hàm khác

Một thực tế khác ở đây là phạm vi của createCounter vẫn tồn tại sau khi nó kết thúc và được sử dụng để chạy đồng thời bởi 2 hàm Lamba

Side Effect và Java

Bây giờ hãy thực hiện tương tự trong Java

public static Map<String, Supplier> createCounter(int initValue) { // the enclosing scope
    int count = initValue;
    Map<String, Supplier> map = new HashMap<>();
    map.put("val", () -> count);
    map.put("inc", () -> count++);
    return map;
}

Code này không biên dịch bởi vì hàm Lamba thứ 2 đang thay đổi biến count

Java lưu các biến của hàm trong Stack, những biến đó sẽ được loại bỏ khi kết thúc hàm createCounter.Các Lamba được tạo sử dụng bản sao của phiên bản count. Nếu trình biên dịch cho phép Lamba thứ 2 thay đổi phiên bản count được sao chép, nó sẽ gây ra khó hiểu.

Để hỗ trợ kiểu Closure này, Java nên lưu các phạm vi đóng trong bộ nhớ Heap và cho phép chúng tồn tại sau khi hàm kết thúc

Java Closure sử dụng mutable object

Như chúng ta đã biết, giá trị của biến sử dụng được sao chép vào biểu thức Lamba Expression (hoặc  anonymous class) , nhưng khi chúng ta sử dụng object thì sao? Trong trường hợp này chỉ có tham chiếu được sao chép và chúng ta có thể thấy một số điểm khác ở đây

Chúng ta có thể mô phỏng Closure của Javascript theo cách này

private static class MyClosure {
    public int value;
    public MyClosure(int initValue) { this.value = initValue; }
}
public static Map<String, Supplier> createCounter(int initValue) {
    MyClosure closure = new MyClosure(initValue);
    Map<String, Supplier> counter = new HashMap<>();
    counter.put("val", () -> closure.value);
    counter.put("inc", () -> closure.value++);
    return counter;
}
Supplier[] v = createCounter(42);
v.get("val").get(); // returns 42
v.get("inc").get(); // returns 42
v.get("val").get(); // returns 43

Cách này trông không thực sự hữu ích và nó thực sự là không phù hợp

Clouse được sử dụng bởi Javascript như một cơ chế cơ bản để tạo các instance: các object. Đây là lý do tại sao mà trong Javascript, một hàm như là MyCounter được gọi là một “Hàm constructor”

Ngược lại Java đã có các class, chúng ta có thể tạo các object theo cách chuẩn chỉnh hơn nhiều

Trong ví dụ trước, chúng ta không cần một Closure. “Hàm factory” đó thực chất là một ví dụ kỳ lạ về cách định nghĩa class. Trong Java, chúng ta có thể định nghĩa một class đơn giản như sau

class MyJavaCounter {
    private int value;
    public MyJavaCounter(int initValue) { this.value = initValue; }
    public int increment() { return value++; }
    public int get() { return value; }
}
MyJavaCounter v = new MyJavaCounter(42);
System.out.println(v.get());       // returns 42
System.out.println(v.increment()); // returns 42
System.out.println(v.get());       // returns 43

Thay đổi các biến độc lập là không nên

Các hàm Lamba thay đổi các biến độc lập có thể gây nên sự khó hiểu. Side Effect của các hàm khác có thể tạo ra một số lỗi không mong muốn. Đây là một trong nhưng lỗi điển hình của các lập trình viên trước đây, những người không hiểu tại sao Javascript tạo ra các hoạt động mà không thể giải thích được. Trong các ngôn ngữ functional, nó thường bị giới hạn, nếu không thì nó cũng không được khuyến khích.

Cân nhắc khi bạn sử dụng để xử lý song song như trong Spark

int counter = 0;
JavaRDD rdd = sc.parallelize(data);
rdd.foreach(x -> counter += x); // không nên thực hiện như thế này

 

 

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.