JavaRush /Blog Java /Random-VI /Phổ biến về biểu thức lambda trong Java. Với các ví dụ và...
Стас Пасинков
Mức độ
Киев

Phổ biến về 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?
  • Dành cho những ai đọc phần đầu của bài viết này;

  • Dành cho những người nghĩ rằng họ đã biết rõ về Java Core nhưng chưa biết gì về biểu thức lambda trong Java. Hoặc có lẽ bạn đã nghe điều gì đó về lambdas nhưng không có thông tin chi tiết.

  • dành cho những người đã có chút hiểu biết về biểu thức lambda nhưng vẫn còn e ngại và chưa quen khi sử dụng chúng.

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

Mã này có được biên dịch với một lớp ẩn danh không?
int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};
KHÔNG. Biến counterphải là final. Hoặc không nhất thiết final, nhưng trong mọi trường hợp nó không thể thay đổi giá trị của nó. Nguyên tắc tương tự được sử dụng trong biểu thức lambda. Họ có quyền truy cập vào tất cả các biến mà họ "hiển thị" từ nơi chúng được khai báo. Nhưng lambda không nên thay đổi chúng (gán giá trị mới). Đúng, có một tùy chọn để vượt qua giới hạn này trong các lớp ẩn danh. Chỉ cần tạo một biến có kiểu tham chiếu và thay đổi trạng thái bên trong của đối tượng là đủ. Trong trường hợp này, chính biến đó sẽ trỏ đến cùng một đối tượng và trong trường hợp này bạn có thể chỉ ra nó một cách an toàn là final.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Ở đây biến của chúng tôi counterlà một tham chiếu đến một đối tượng thuộc loại AtomicInteger. Và để thay đổi trạng thái của đối tượng này, phương thức này được sử dụng incrementAndGet(). Bản thân giá trị của biến không thay đổi trong khi chương trình đang chạy và luôn trỏ đến cùng một đối tượng, điều này cho phép chúng ta khai báo một biến ngay lập tức bằng từ khóa final. Các ví dụ tương tự, nhưng với biểu thức lambda:
int counter = 0;
Runnable r = () -> counter++;
Nó không biên dịch vì lý do tương tự như tùy chọn có lớp ẩn danh: counternó không nên thay đổi trong khi chương trình đang chạy. Nhưng như thế này - mọi thứ đều ổn:
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. Từ bên trong 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 các phương thức mà bạn có quyền truy cập.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
Mặc dù phương thức này staticMethod()là riêng tư nhưng nó “có thể truy cập được” để được gọi bên trong phương thức main(), do đó, cũng có thể truy cập được để gọi từ bên trong lambda được tạo trong phương thức đó main.

Thời điểm thực thi mã biểu thức lambda

Câu hỏi này có vẻ quá đơn giản đối với bạn, nhưng cũng đáng hỏi: khi nào mã bên trong biểu thức lambda sẽ được thực thi? Tại thời điểm sáng tạo? Hoặc tại thời điểm (vẫn chưa biết ở đâu) nó sẽ được gọi? Nó khá dễ dàng để kiểm tra.
System.out.println("Запуск программы");

// много всякого разного codeа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого codeа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
Đầu ra trên màn hình:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Có thể thấy rằng mã biểu thức lambda được thực thi ở cuối, sau khi luồng được tạo và chỉ khi quá trình thực thi chương trình đạt đến mức thực thi thực tế của phương thức run(). Và hoàn toàn không phải tại thời điểm công bố. Bằng cách khai báo biểu thức lambda, chúng tôi chỉ tạo một đối tượng thuộc loại Runnablevà mô tả hành vi của phương thức của nó run(). Bản thân phương pháp này đã được đưa ra muộn hơn nhiều.

Tài liệu tham khảo phương pháp?

Bản thân nó không liên quan trực tiếp đến lambdas, nhưng tôi nghĩ sẽ hợp lý nếu nói vài lời về nó 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 số phương thức.
x -> System.out.println(x)
Họ đưa cho anh ấy một thứ gì đó х, và nó chỉ đơn giản gọi anh ấy System.out.println()và đưa anh ấy đến đó х. Trong trường hợp này, chúng ta có thể thay thế nó bằng một liên kết đến phương thức chúng ta cần. Như thế này:
System.out::println
Có, không có dấu ngoặc đơn ở cuối! Ví dụ đầy đủ hơn:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(x -> System.out.println(x));
Ở dòng cuối cùng, chúng tôi sử dụng một phương thức forEach()chấp nhận một đối tượng giao diện Consumer. Đây lại là một giao diện chức năng chỉ có một phương thức void accept(T t). Theo đó, chúng ta viết một biểu thức lambda nhận một tham số (vì nó được nhập vào chính giao diện nên chúng ta không chỉ ra loại tham số mà chỉ ra rằng nó sẽ được gọi х). Trong phần nội dung của biểu thức lambda, chúng ta viết mã nó sẽ được thực thi khi phương thức được gọi accept(). Ở đây chúng ta chỉ hiển thị trên màn hình những gì có trong biến х. Bản thân phương thức này forEach()đi qua tất cả các phần tử của bộ sưu tập, gọi Consumerphương thức của đối tượng giao diện được truyền cho nó (lambda của chúng ta) accept(), nơi nó chuyển từng phần tử từ bộ sưu tập. Như tôi đã nói, đây là biểu thức lambda - (chỉ cần gọi một phương thức khác) mà chúng ta có thể thay thế bằng một tham chiếu đến phương thức chúng ta cần. Sau đó, mã của chúng ta sẽ trông như thế này:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

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

Cú pháp sử dụng tham chiếu phương thức

Nó khá đơn giản:
  1. Truyền tham chiếu đến phương thức tĩnhNameКласса:: NameСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. Truyền tham chiếu đến một phương thức không tĩnh bằng đối tượng hiện cóNameПеременнойСОбъектом:: method name

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            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 mà phương thức đó được triển khaiNameКласса:: method name

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Vasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            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. Truyền một liên kết đến hàm tạo NameКласса::new
    Sử dụng các liên kết phương thức rất thuận tiện khi có một phương thức làm sẵn mà bạn hoàn toàn hài lòng và bạn muốn sử dụng nó làm phương thức gọi lại. Trong trường hợp này, thay vì viết biểu thức lambda với mã của phương thức đó hoặc biểu thức lambda mà chúng ta chỉ gọi phương thức này, chúng ta chỉ cần chuyển một tham chiếu đến nó. Đó là tất cả.

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

Trong lớp ẩn danh, từ khóa trỏ thisđến đối tượng của lớp ẩn danh đó. Và nếu chúng ta sử dụng nó thisbên trong lambda, chúng ta sẽ có quyền truy cập vào đối tượng của lớp đóng khung. Nơi chúng tôi thực sự đã viết biểu thức này. Điều này xảy ra vì các biểu thức lambda khi được biên dịch sẽ trở thành một phương thức riêng tư của lớp nơi chúng được viết. 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ụ, mâu thuẫn với các nguyên tắc lập trình chức năng. Nhưng cách tiếp cận này khá phù hợp với OOP. ;)

Tôi lấy thông tin từ đâu hoặc đọc gì khác

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION