JavaRush /Java Blog /Random-KO /ArrayList.forEach의 람다 및 메서드 참조 - 작동 방식

ArrayList.forEach의 람다 및 메서드 참조 - 작동 방식

Random-KO 그룹에 게시되었습니다
Java Syntax Zero 퀘스트의 람다 식 소개는 매우 구체적인 예에서 시작됩니다.
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
강의 작성자는 ArrayList 클래스의 표준 forEach 함수를 사용하여 람다 및 메서드 참조를 구문 분석합니다. 개인적으로 저는 이 기능의 구현과 이와 관련된 인터페이스가 "내부적으로" 남아 있기 때문에 무슨 일이 일어나고 있는지 이해하기 어렵다는 것을 알았습니다. 인수 (들)가 에서 오는 곳 , println() 함수가 전달되는 곳 은 우리가 스스로 대답해야 할 질문입니다. 다행히 IntelliJ IDEA를 사용하면 ArrayList 클래스의 내부를 쉽게 조사하고 처음부터 이 문제를 풀 수 있습니다. 당신도 아무것도 이해하지 못하고 이해하고 싶다면 조금이라도 도와 드리겠습니다. 람다 표현식과 ArrayList.forEach - 작동 방식 강의를 통해 우리는 람다 표현식이 기능적 인터페이스의 구현이라는 것을 이미 알고 있습니다 . 즉, 하나의 단일 함수로 인터페이스를 선언하고 람다를 사용하여 이 함수의 기능을 설명합니다. 이를 수행하려면 다음이 필요합니다. 1. 기능적 인터페이스를 생성합니다. 2. 기능적 인터페이스에 해당하는 유형의 변수를 생성합니다. 3. 이 변수에 함수 구현을 설명하는 람다 식을 할당합니다. 4. 변수에 접근하여 함수를 호출합니다. (어쩌면 용어가 엉성할 수도 있지만 이것이 가장 명확한 방법입니다.) 자세한 설명과 함께 Google의 간단한 예를 들어보겠습니다(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
    }
}
이제 강의의 예로 돌아가 보겠습니다. String 유형의 여러 요소가 목록 컬렉션 에 추가됩니다 . 그런 다음 목록 개체 에서 호출되는 표준 forEach 함수를 사용하여 요소를 검색합니다 . 이상한 매개변수가 포함된 람다 표현식이 함수 에 인수로 전달됩니다 .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
여기서 일어난 일을 즉시 이해하지 못했다면 혼자가 아닙니다. 다행히 IntelliJ IDEA에는 Ctrl+Left_Mouse_Button 이라는 훌륭한 키보드 단축키가 있습니다 . forEach 위에 마우스를 놓고 이 조합을 클릭하면 표준 ArrayList 클래스의 소스 코드가 열리고 여기서 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();
}
입력 인수가 Consumer 유형의 작업임을 알 수 있습니다 . Consumer 라는 단어 위로 커서를 이동 하고 마법 조합인 Ctrl+LMB를 다시 눌러 보겠습니다 . 소비자 인터페이스 에 대한 설명이 열립니다 . 기본 구현을 제거하면(지금은 중요하지 않음) 다음 코드가 표시됩니다.
public interface Consumer<t> {
   void accept(T t);
}
그래서. 우리는 모든 유형의 하나의 인수를 허용하는 단일 accept 함수를 가진 Consumer 인터페이스를 가지고 있습니다 . 함수가 하나만 있으므로 인터페이스는 기능적이며 구현은 람다 식을 통해 작성할 수 있습니다. 우리는 ArrayList에 Consumer 인터페이스 의 구현을 액션 인수 로 취하는 forEach 함수가 있다는 것을 이미 보았습니다 . 또한 forEach 함수에는 다음 코드가 있습니다.
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
for 루프는 기본적으로 ArrayList의 모든 요소를 ​​반복합니다. 루프 내부에서는 액션 객체 의 accept 함수에 대한 호출을 볼 수 있습니다 . Operation.calculate를 어떻게 호출했는지 기억하시나요? 컬렉션의 현재 요소가 accept 함수 에 전달됩니다 . 이제 우리는 마침내 원래의 람다 표현식으로 돌아가서 그것이 무엇을 하는지 이해할 수 있습니다. 모든 코드를 하나의 더미로 모아봅시다:
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) );
우리의 람다 표현식은 Consumer 인터페이스 에 설명된 accept 함수 의 구현입니다 . 람다를 사용하여 accept 함수가 인수 s를 가져와 화면에 표시하도록 지정했습니다. 람다 식은 Consumer 인터페이스 의 구현을 저장하는 작업 인수 로 forEach 함수에 전달되었습니다 . 이제 forEach 함수는 다음과 같은 줄을 사용하여 Consumer 인터페이스 구현을 호출할 수 있습니다.
action.accept(elementAt(es, i));
따라서 람다 표현식의 입력 인수 s는 ArrayList 컬렉션의 또 다른 요소 이며 Consumer 인터페이스 구현에 전달됩니다 . 그게 전부입니다. ArrayList.forEach에서 람다 표현식의 논리를 분석했습니다. ArrayList.forEach의 메소드 참조 - 어떻게 작동하나요? 강의의 다음 단계는 메소드 참조를 살펴보는 것입니다. 사실, 그들은 그것을 매우 이상한 방식으로 이해합니다. 강의를 읽은 후에 나는 이 코드가 무엇을 하는지 이해할 기회가 없었습니다.
list.forEach( System.out::println );
첫째, 다시 약간의 이론입니다. 메소드 참조는 매우 대략적으로 말하면 다른 함수에 의해 설명되는 기능적 인터페이스의 구현입니다 . 다시 한 번 간단한 예부터 시작하겠습니다.
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
}
강의의 예로 돌아가 보겠습니다.
list.forEach( System.out::println );
System.out은 println 함수 가 있는 PrintStream 유형의 개체라는 점을 상기시켜 드리겠습니다 . println 위에 마우스를 놓고 Ctrl+LMB를 클릭해 보겠습니다 .
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
두 가지 주요 기능을 살펴보겠습니다. 1. println 함수는 아무것도 반환하지 않습니다(void). 2. println 함수는 하나의 인수를 입력으로 받습니다. 아무것도 생각나지 않나요?
public interface Consumer<t> {
   void accept(T t);
}
맞습니다. accept 함수 서명은 println 메서드 서명 의 보다 일반적인 경우입니다 ! 이는 후자가 메소드에 대한 참조로 성공적으로 사용될 수 있음을 의미합니다. 즉, println은 accept 함수의 특정 구현이 됩니다 .
list.forEach( System.out::println );
System.out 객체의 println 함수를 forEach 함수 의 인수로 전달했습니다 . 원칙은 람다와 동일합니다. 이제 forEach는 action.accept(elementAt(es, i)) 호출을 통해 컬렉션 요소를 println 함수에 전달할 수 있습니다 . 실제로 이는 이제 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();
    }
람다와 메서드 참조를 처음 접하는 사람들을 위해 상황을 조금이라도 명확하게 설명했으면 좋겠습니다. 결론적으로 저는 Robert Schildt의 유명한 책 "Java: A Beginner's Guide"를 추천합니다. 제 생각에는 이 책에 람다와 함수 참조가 아주 합리적으로 설명되어 있습니다.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION