CodeGym /Blog Java /Ngẫu nhiên /Giải thích về các biểu thức lambda trong Java. Với các ví...
John Squirrels
Mức độ
San Francisco

Giải thích về các biểu thức lambda trong Java. Với các ví dụ và nhiệm vụ. Phần 2

Xuất bản trong nhóm
Bài viết này dành cho ai?
  • Nó dành cho những người đọc phần đầu tiên của bài viết này;
  • Nó dành cho những người nghĩ rằng họ đã biết rõ về Java Core, nhưng không biết gì về các biểu thức lambda trong Java. Hoặc có thể họ đã nghe điều gì đó về biểu thức lambda nhưng thiếu thông tin chi tiết.
  • Nó dành cho những người có hiểu biết nhất định về các biểu thức lambda, nhưng vẫn cảm thấy khó khăn và không quen sử dụng chúng.
Nếu bạn không phù hợp với một trong những loại này, bạn có thể thấy bài viết này nhàm chán, thiếu sót hoặc nói chung không phải là tách trà của bạn. Trong trường hợp này, vui lòng chuyển sang chủ đề khác hoặc nếu bạn thông thạo về chủ đề này, vui lòng đưa ra đề xuất trong phần nhận xét về cách tôi có thể cải thiện hoặc bổ sung bài viết. Giải thích về các biểu thức lambda trong Java.  Với các ví dụ và nhiệm vụ.  Phần 2 - 1Tài liệu không tuyên bố là có bất kỳ giá trị học thuật nào, chứ chưa nói đến tính mới. Hoàn toàn ngược lại: Tôi sẽ cố gắng mô tả những thứ phức tạp (đối với một số người) một cách đơn giản nhất có thể. Yêu cầu giải thích về Stream API đã thôi thúc tôi viết bài này. Tôi đã nghĩ về điều đó và quyết định rằng một số ví dụ về luồng của tôi sẽ không thể hiểu được nếu không hiểu các biểu thức lambda. Vì vậy, chúng ta sẽ bắt đầu với các biểu thức lambda.

Truy cập vào các biến bên ngoài

Mã này có biên dịch với lớp ẩn danh không?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
Không. counter Biến phải là final. Hoặc nếu không final, thì ít nhất nó không thể thay đổi giá trị của nó. Nguyên tắc tương tự cũng được áp dụng trong các biểu thức lambda. Họ có thể truy cập tất cả các biến mà họ có thể "nhìn thấy" từ nơi chúng được khai báo. Nhưng lambda không được thay đổi chúng (gán giá trị mới cho chúng). Tuy nhiên, có một cách để bỏ qua hạn chế này trong các lớp ẩn danh. Chỉ cần tạo một biến tham chiếu và thay đổi trạng thái bên trong của đối tượng. Khi làm như vậy, bản thân biến không thay đổi (trỏ đến cùng một đối tượng) và có thể được đánh dấu an toàn là final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Ở đây counterbiến của chúng ta là một tham chiếu đến một AtomicIntegerđối tượng. Và incrementAndGet()phương thức được sử dụng để thay đổi trạng thái của đối tượng này. Giá trị của biến tự nó không thay đổi trong khi chương trình đang chạy. Nó luôn trỏ đến cùng một đối tượng, cho phép chúng ta khai báo biến với từ khóa cuối cùng. Dưới đây là các ví dụ tương tự, nhưng với các biểu thức lambda:

int counter = 0;
Runnable r = () -> counter++;
Điều này sẽ không biên dịch vì lý do tương tự như phiên bản có lớp ẩn danh:  counterkhông được thay đổi trong khi chương trình đang chạy. Nhưng mọi thứ đều ổn nếu chúng ta làm như thế này:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Điều này cũng áp dụng cho các phương thức gọi. Trong các biểu thức lambda, bạn không chỉ có thể truy cập tất cả các biến "hiển thị" mà còn có thể gọi bất kỳ phương thức nào có thể truy cập được.

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
Mặc dù staticMethod()là riêng tư, nhưng nó có thể truy cập được bên trong main()phương thức, vì vậy nó cũng có thể được gọi từ bên trong lambda được tạo trong mainphương thức.

Khi nào một biểu thức lambda được thực thi?

Bạn có thể thấy câu hỏi sau đây quá đơn giản, nhưng bạn cũng nên hỏi nó giống như vậy: khi nào mã bên trong biểu thức lambda sẽ được thực thi? Khi nó được tạo ra? Hoặc khi nó được gọi (chưa được biết)? Điều này là khá dễ dàng để kiểm tra.

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
Đầu ra màn hình:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Bạn có thể thấy rằng biểu thức lambda đã được thực thi ở cuối, sau khi chuỗi được tạo và chỉ khi quá trình thực thi của chương trình đạt đến phương thức run(). Chắc chắn không phải khi nó được tuyên bố. Bằng cách khai báo một biểu thức lambda, chúng ta chỉ tạo một Runnableđối tượng và mô tả cách thức run()hoạt động của phương thức đó. Bản thân phương thức này được thực thi muộn hơn nhiều.

Phương pháp tham khảo?

Các tham chiếu phương thức không liên quan trực tiếp đến lambda, nhưng tôi nghĩ nên nói một vài lời về chúng trong bài viết này. Giả sử chúng ta có một biểu thức lambda không làm gì đặc biệt mà chỉ gọi một phương thức.

x -> System.out.println(x)
Nó nhận được một số xvà chỉ gọi System.out.println(), chuyển vào x. Trong trường hợp này, chúng ta có thể thay thế nó bằng một tham chiếu đến phương thức mong muốn. Như thế này:

System.out::println
Đúng vậy - không có dấu ngoặc đơn ở cuối! Đây là một ví dụ đầy đủ hơn:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
Ở dòng cuối cùng, chúng ta sử dụng forEach()phương thức, phương thức này nhận một đối tượng cài đặt Consumergiao diện. Một lần nữa, đây là một giao diện chức năng, chỉ có một void accept(T t)phương thức. Theo đó, chúng tôi viết một biểu thức lambda có một tham số (vì nó được nhập trong chính giao diện, chúng tôi không chỉ định loại tham số; chúng tôi chỉ cho biết rằng chúng tôi sẽ gọi nó x). Trong phần thân của biểu thức lambda, chúng ta viết mã sẽ được thực thi khi phương accept()thức được gọi. Ở đây chúng tôi chỉ hiển thị những gì kết thúc trong xbiến. Phương thức tương tự này forEach()lặp qua tất cả các phần tử trong bộ sưu tập và gọi accept()phương thức khi triển khaiConsumerinterface (lambda của chúng tôi), chuyển từng mục trong bộ sưu tập. Như tôi đã nói, chúng ta có thể thay thế một biểu thức lambda như vậy (một biểu thức chỉ đơn giản là phân loại một phương thức khác) bằng một tham chiếu đến phương thức mong muốn. Sau đó, mã của chúng tôi sẽ trông như thế này:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
Điều chính là các tham số của phương thức println()accept()khớp với nhau. Bởi vì println()phương thức có thể chấp nhận mọi thứ (nó bị quá tải đối với tất cả các kiểu nguyên thủy và tất cả các đối tượng), thay vì các biểu thức lambda, chúng ta chỉ cần chuyển một tham chiếu đến phương println()thức tới forEach(). Sau đó, forEach()sẽ lấy từng phần tử trong bộ sưu tập và chuyển trực tiếp cho println()phương thức. Đối với bất kỳ ai gặp phải điều này lần đầu tiên, xin lưu ý rằng chúng tôi không gọi System.out.println()(có dấu chấm giữa các từ và có dấu ngoặc đơn ở cuối). Thay vào đó, chúng tôi đang chuyển một tham chiếu đến phương thức này. Nếu chúng ta viết điều này

strings.forEach(System.out.println());
chúng tôi sẽ có một lỗi biên dịch. Trước khi gọi đến forEach(), Java thấy rằng nó System.out.println()đang được gọi, vì vậy nó hiểu rằng giá trị trả về là voidvà sẽ cố gắng chuyển voidđến forEach(), thay vào đó là mong đợi một Consumerđối tượng.

Cú pháp tham chiếu phương thức

Nó khá đơn giản:
  1. Chúng tôi chuyển một tham chiếu đến một phương thức tĩnh như thế này:ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. Chúng tôi chuyển một tham chiếu đến một phương thức không tĩnh bằng một đối tượng hiện có, như thế này:objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            Main instance = new Main(); 
            strings.forEach(instance::nonStaticMethod); 
        } 
    
        private void nonStaticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  3. Chúng tôi chuyển một tham chiếu đến một phương thức không tĩnh bằng cách sử dụng lớp thực hiện nó như sau:ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            users.forEach(User::print); 
        } 
    
        private static class User { 
            private String name; 
    
            private User(String name) { 
                this.name = name; 
            } 
    
            private void print() { 
                System.out.println(name); 
            } 
        } 
    }
    
  4. Chúng tôi chuyển một tham chiếu đến một hàm tạo như thế này:ClassName::new

    Các tham chiếu phương thức rất thuận tiện khi bạn đã có một phương thức hoạt động hoàn hảo như một cuộc gọi lại. Trong trường hợp này, thay vì viết biểu thức lambda chứa mã của phương thức hoặc viết biểu thức lambda chỉ gọi phương thức, chúng ta chỉ cần chuyển tham chiếu đến phương thức đó. Và thế là xong.

Một sự khác biệt thú vị giữa các lớp ẩn danh và biểu thức lambda

Trong một lớp ẩn danh, thistừ khóa trỏ đến một đối tượng của lớp ẩn danh. Nhưng nếu chúng ta sử dụng this bên trong lambda, chúng ta sẽ có quyền truy cập vào đối tượng của lớp chứa. Cái mà chúng ta thực sự đã viết biểu thức lambda. Điều này xảy ra vì các biểu thức lambda được biên dịch thành một phương thức riêng của lớp mà chúng được viết vào. Tôi không khuyên bạn nên sử dụng "tính năng" này, vì nó có tác dụng phụ và điều đó mâu thuẫn với các nguyên tắc của lập trình hàm. Điều đó nói rằng, cách tiếp cận này hoàn toàn phù hợp với OOP. ;)

Tôi đã lấy thông tin của mình ở đâu và bạn nên đọc gì khác?

Và, tất nhiên, tôi đã tìm thấy rất nhiều thứ trên Google :)
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION