JavaRush /Blog Java /Random-VI /Lambdas và tham chiếu phương thức trong ArrayList.forEach...
Anonymous #2633326
Mức độ

Lambdas và tham chiếu phương thức trong ArrayList.forEach - cách thức hoạt động

Xuất bản trong nhóm
Phần giới thiệu về biểu thức lambda trong nhiệm vụ Java Syntax Zero bắt đầu bằng một ví dụ rất cụ thể:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Các tác giả của bài giảng phân tích các lambda và tham chiếu phương thức bằng cách sử dụng hàm forEach tiêu chuẩn của lớp ArrayList. Cá nhân tôi cảm thấy khó hiểu ý nghĩa của những gì đang xảy ra, vì việc triển khai chức năng này cũng như giao diện liên quan đến nó vẫn còn “ẩn”. (Các) đối số xuất phát từ đâu , hàm println() được truyền vào đâu là những câu hỏi mà chúng ta sẽ phải tự trả lời. May mắn thay, với IntelliJ IDEA, chúng ta có thể dễ dàng xem xét các phần bên trong của lớp ArrayList và giải quyết vấn đề này ngay từ đầu. Nếu bạn cũng không hiểu bất cứ điều gì và muốn tìm hiểu nó, tôi sẽ cố gắng giúp bạn điều này ít nhất một chút. Biểu thức Lambda và ArrayList.forEach - cách thức hoạt động Từ bài giảng, chúng ta đã biết rằng biểu thức lambda là một triển khai của một giao diện chức năng . Tức là chúng ta khai báo một giao diện với một hàm duy nhất và sử dụng lambda để mô tả chức năng của hàm này. Để làm điều này bạn cần: 1. Tạo một giao diện chức năng; 2. Tạo một biến có kiểu tương ứng với giao diện chức năng; 3. Gán cho biến này một biểu thức lambda mô tả việc triển khai hàm; 4. Gọi một hàm bằng cách truy cập một biến (có lẽ tôi dùng thuật ngữ thô sơ, nhưng đây là cách rõ ràng nhất). Tôi sẽ đưa ra một ví dụ đơn giản từ Google, cung cấp cho nó các nhận xét chi tiết (cảm ơn các tác giả của trang web metanit.com):
interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс,
    // который можно реализовать с помощью лямбды
}

public class LambdaApp {

    public static void main(String[] args) {

        // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
        Operationable operation;
        // Прописываем реализацию функции calculate с помощью лямбды, на вход подаём x и y, на выходе возвращаем их сумму
        operation = (x,y)->x+y;

        // Теперь мы можем обратиться к функции calculate через переменную operation
        int result = operation.calculate(10, 20);
        System.out.println(result); //30
    }
}
Bây giờ chúng ta hãy quay lại ví dụ từ bài giảng. Một số phần tử kiểu String được thêm vào bộ sưu tập danh sách . Sau đó, các phần tử được truy xuất bằng hàm forEach tiêu chuẩn , được gọi trên đối tượng danh sách . Một biểu thức lambda với một số tham số lạ s được truyền làm đối số cho hàm .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Nếu bạn không hiểu ngay chuyện gì đã xảy ra ở đây thì bạn không đơn độc. May mắn thay, IntelliJ IDEA có một phím tắt tuyệt vời: Ctrl+Left_Mouse_Button . Nếu chúng ta di chuột qua forEach và nhấp vào tổ hợp này, mã nguồn của lớp ArrayList tiêu chuẩn sẽ mở ra, trong đó chúng ta sẽ thấy cách triển khai phương thức forEach :
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i));
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
Chúng tôi thấy rằng đối số đầu vào là hành động thuộc loại Consumer . Hãy di chuyển con trỏ qua từ Consumer và nhấn lại tổ hợp thần kỳ Ctrl+LMB . Mô tả về giao diện Người tiêu dùng sẽ mở ra . Nếu chúng tôi xóa phần triển khai mặc định khỏi nó (hiện tại nó không quan trọng đối với chúng tôi), chúng tôi sẽ thấy đoạn mã sau:
public interface Consumer<t> {
   void accept(T t);
}
Vì thế. Chúng tôi có giao diện Người tiêu dùng với một hàm chấp nhận duy nhất chấp nhận một đối số thuộc bất kỳ loại nào. Vì chỉ có một chức năng nên giao diện có chức năng và việc triển khai nó có thể được viết thông qua biểu thức lambda. Chúng ta đã thấy rằng ArrayList có hàm forEach lấy việc triển khai giao diện Consumer làm đối số hành động . Ngoài ra, trong hàm forEach, chúng ta tìm thấy đoạn mã sau:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
Vòng lặp for về cơ bản lặp qua tất cả các phần tử của ArrayList. Bên trong vòng lặp, chúng ta thấy một lệnh gọi đến hàm chấp nhận của đối tượng hành động - bạn có nhớ cách chúng ta gọi là Operation.calcate không? Phần tử hiện tại của bộ sưu tập được chuyển tới hàm chấp nhận . Bây giờ cuối cùng chúng ta cũng có thể quay lại biểu thức lambda ban đầu và hiểu nó làm gì. Hãy thu thập tất cả mã vào một đống:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована нашей лямбдой {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i)); // Вызываем нашу реализацию функции accept интерфейса Consumer для каждого element коллекции
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

//...

list.forEach( (s) -> System.out.println(s) );
Biểu thức lambda của chúng tôi là cách triển khai hàm chấp nhận được mô tả trong giao diện Người tiêu dùng . Bằng cách sử dụng lambda, chúng tôi đã chỉ định rằng hàm chấp nhận lấy một đối số s và hiển thị nó trên màn hình. Biểu thức lambda được chuyển tới hàm forEach làm đối số hành động của nó , lưu trữ việc triển khai giao diện Người tiêu dùng . Bây giờ, hàm forEach có thể gọi việc triển khai giao diện Người tiêu dùng của chúng ta bằng một dòng như thế này:
action.accept(elementAt(es, i));
Do đó, đối số đầu vào s trong biểu thức lambda là thành phần tiếp theo của bộ sưu tập ArrayList , được chuyển cho quá trình triển khai giao diện Người tiêu dùng của chúng ta . Vậy là xong: chúng ta đã phân tích logic của biểu thức lambda trong ArrayList.forEach. Tham chiếu đến một phương thức trong ArrayList.forEach - nó hoạt động như thế nào? Bước tiếp theo trong bài giảng là xem xét các tài liệu tham khảo về phương pháp. Đúng vậy, họ hiểu nó theo một cách rất kỳ lạ - sau khi đọc bài giảng, tôi không có cơ hội hiểu được đoạn mã này dùng để làm gì:
list.forEach( System.out::println );
Đầu tiên, lại một chút lý thuyết. Nói một cách đại khái, tham chiếu phương thức là việc triển khai một giao diện chức năng được mô tả bởi một hàm khác . Một lần nữa, tôi sẽ bắt đầu với một ví dụ đơn giản:
public interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс
}

public static class Calculator {
    // Создадим статический класс Calculator и пропишем в нём метод methodReference.
    // Именно он будет реализовывать функцию calculate из интерфейса Operationable.
    public static int methodReference(int x, int y) {
        return x+y;
    }
}

public static void main(String[] args) {
    // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
    Operationable operation;
    // Теперь реализацией интерфейса будет не лямбда-выражение, а метод methodReference из нашего класса Calculator
    operation = Calculator::methodReference;

    // Теперь мы можем обратиться к функции интерфейса через переменную operation
    int result = operation.calculate(10, 20);
    System.out.println(result); //30
}
Hãy quay lại ví dụ từ bài giảng:
list.forEach( System.out::println );
Hãy để tôi nhắc bạn rằng System.out là một đối tượng thuộc loại PrintStream có chức năng println . Hãy di chuột qua println và nhấp Ctrl+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Hãy lưu ý hai tính năng chính: 1. Hàm println không trả về bất cứ thứ gì (void). 2. Hàm println nhận một đối số làm đầu vào. Không nhắc nhở bạn về bất cứ điều gì?
public interface Consumer<t> {
   void accept(T t);
}
Đúng vậy - chữ ký hàm chấp nhận là trường hợp tổng quát hơn của chữ ký phương thức println ! Điều này có nghĩa là cái sau có thể được sử dụng thành công làm tham chiếu đến một phương thức - nghĩa là println trở thành một triển khai cụ thể của hàm chấp nhận :
list.forEach( System.out::println );
Chúng ta đã chuyển hàm println của đối tượng System.out làm đối số cho hàm forEach . Nguyên tắc này giống như với lambda: bây giờ forEach có thể chuyển một phần tử bộ sưu tập tới hàm println thông qua lệnh gọi action.accept(elementAt(es, i)) . Trên thực tế, điều này bây giờ có thể được đọc là System.out.println(elementAt(es, i)) .
public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована методом println {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        final Object[] es = elementData;
        final int size = this.size;
        for (int i = 0; modCount == expectedModCount && i < size; i++)
            action.accept(elementAt(es, i)); // Функция accept теперь реализована методом System.out.println!
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
Tôi hy vọng rằng tôi đã làm rõ tình huống này ít nhất một chút cho những người mới làm quen với lambdas và các tài liệu tham khảo về phương pháp. Để kết luận, tôi giới thiệu cuốn sách nổi tiếng "Java: Hướng dẫn cho người mới bắt đầu" của Robert Schildt - theo ý kiến ​​​​của tôi, lambdas và các tài liệu tham khảo chức năng được mô tả khá hợp lý trong đó.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION