JavaRush /Java Blog /Random-JA /ArrayList.forEach のラムダとメソッド参照 - その仕組み
Anonymous #2633326
レベル 20

ArrayList.forEach のラムダとメソッド参照 - その仕組み

Random-JA グループに公開済み
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 つの関数を含むインターフェイスを宣言し、ラムダを使用してこの関数が何を行うかを記述します。これを行うには、次のことが必要です。 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をもう一度押してみましょう。Consumerインターフェースの説明が開きます。そこからデフォルトの実装を削除すると (現時点では重要ではありません)、次のコードが表示されます。
public interface Consumer<t> {
   void accept(T t);
}
それで。任意の型の 1 つの引数を受け入れる単一のaccept関数を備えたConsumerインターフェイスがあります。関数が 1 つだけあるため、インターフェイスは機能し、その実装はラムダ式を通じて記述できます。ArrayList には、 Consumerインターフェイスの実装をアクション引数として受け取るforEach関数があることはすでに説明しました。さらに、forEach 関数には次のコードがあります。
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
for ループは基本的に、ArrayList のすべての要素を反復処理します。ループ内では、アクションオブジェクトのaccept関数の呼び出しが見られます。operation.calculate をどのように呼び出したか覚えていますか? コレクションの現在の要素がaccept関数に渡されます。これで、ようやく元のラムダ式に戻って、その式が何をするのかを理解できるようになりました。すべてのコードを 1 つの山にまとめてみましょう。
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を受け取り、それを画面に表示するように指定しました。ラムダ式は、アクション引数としてforEach関数に渡され、 Consumerインターフェイスの実装が格納されます。これで、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();
        }
    }
}
2 つの重要な機能に注目してください。 1. println関数は何も返しません (void)。2. println関数は、入力として 1 つの引数を受け取ります。何か思い出しませんか?
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