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