ラムダコールはどのようにインターフェイスと相互作用しますか?


8

以下に示すコードスニペットは機能します。ただし、なぜ機能するのかはわかりません。ラムダ関数が情報をインターフェイスに渡す方法のロジックを完全には理解していません。

制御はどこに渡されますか?コンパイラーnは、ループ内のそれぞれとmessage作成されたものをどのように理解していますか?

このコードはコンパイルされ、期待される結果が得られます。どうすればいいのか分かりません。

import java.util.ArrayList;
import java.util.List;

public class TesterClass {

    public static void main(String[] args) {

        List<String> names = new ArrayList<>();

        names.add("Akira");
        names.add("Jacky");
        names.add("Sarah");
        names.add("Wolf");

        names.forEach((n) -> {
            SayHello hello = (message) -> System.out.println("Hello " + message);
            hello.speak(n);
        });
    }

    interface SayHello {
        void speak(String message);
    }
}

回答:


8

SayHello文字列を受け取り、voidを返すメソッドを持つ単一の抽象メソッドのインタフェースです。これは消費者に似ています。次の匿名の内部クラス実装と同様の、コンシューマーの形式でそのメソッドの実装を提供しているだけです。

SayHello sh = new SayHello() {
    @Override
    public void speak(String message) {
        System.out.println("Hello " + message);
    }
};

names.forEach(n -> sh.speak(n));

幸い、インターフェースには単一のメソッドがあり、型システム(より正式には、型解決アルゴリズム)はその型をと推測できSayHelloます。しかし、2つ以上のメソッドがある場合、これは不可能です。

ただし、はるかに優れたアプローチは、forループの前にコンシューマーを宣言し、以下に示すように使用することです。各反復の実装を宣言すると、必要以上のオブジェクトが作成され、私には直観に反するように見えます。以下は、ラムダの代わりにメソッド参照を使用する拡張バージョンです。ここで使用される制限付きメソッド参照は、hello上記で宣言されたインスタンスの関連メソッドを呼び出します。

SayHello hello = message -> System.out.println("Hello " + message);
names.forEach(hello::speak);

更新

インスタンスが作成されると一度だけレキシカルスコープから何もキャプチャしないステートレスラムダの場合、どちらのアプローチもの1つのインスタンスを作成するだけSayHelloであり、提案されたアプローチに従ってもメリットはありません。しかし、これは実装の詳細のようで、今まで知りませんでした。したがって、はるかに優れたアプローチは、以下のコメントで提案されているように、コンシューマーをforEachに渡すことです。また、これらのアプローチはすべて、SayHelloインターフェースのインスタンスを1つだけ作成しますが、最後のインスタンスはより簡潔です。これがその様子です。

names.forEach(message -> System.out.println("Hello " + message));

この答えは、あなたにそれについてより多くの洞察を与えます。JLS§15.27.4の関連セクションは次のとおりです。ラムダ式の実行時評価

これらのルールは、Javaプログラミング言語の実装に柔軟性を提供することを目的としています。

  • すべての評価で新しいオブジェクトを割り当てる必要はありません。

実際、最初はすべての評価が新しいインスタンスを作成すると思っていましたが、これは誤りです。@Holger指摘、ありがとうございます。


わかりました。私は単にコンシューマーインターフェイスのバージョンを作成しました。しかし、ループに入れたので、ループごとに個別のオブジェクトを作成し、メモリを消費しました。一方、実装はその1つのオブジェクトを作成し、各ループで「メッセージ」を交換します。ご説明ありがとうございます!
Brodiman

1
はい、独自のロールアウトではなく、既存の機能的なインターフェイスを使用できる場合は常に良いです。この場合Consumer<String>sayHelloインターフェースの代わりに使用できます。
Ravindra Ranwala

1
行をどこに配置してもかまいません。インスタンスSayHello hello = message -> System.out.println("Hello " + message);は1つだけSayHelloです。とにかくここで1つのオブジェクトが廃止されたとしても、を介して同じことが可能names.forEach(n -> System.out.println("Hello " + n))であるため、「拡張バージョン」の改善はありません。
ホルガー

@ホルガー私はあなたの提案で答えを更新しました。心から感謝する。したがって、すべての実装はSayHelloの単一のインスタンスを作成しますが、提案されたものはよりコンパクトですか?そうではないですか?
Ravindra Ranwala

1

foreachのシグネチャは次のようなものです。

void forEach(Consumer<? super T> action)

コンシューマーのオブジェクトを取り込みます。コンシューマーインターフェイスは、機能インターフェイス(単一の抽象メソッドを持つインターフェイス)です。入力を受け入れ、結果を返しません。

定義は次のとおりです。

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

したがって、どの実装も、あなたの場合のものは2つの方法で書くことができます:

Consumer<String> printConsumer = new Consumer<String>() {
    public void accept(String name) {
        SayHello hello = (message) -> System.out.println("Hello " + message);
        hello.speak(n);
    };
};

または

(n) -> {
            SayHello hello = (message) -> System.out.println("Hello " + message);
            hello.speak(n);
        }

同様に、SayHelloクラスのコードは次のように書くことができます。

SayHello sh = new SayHello() {
    @Override
    public void speak(String message) {
        System.out.println("Hello " + message);
    }
};

または

SayHello hello = message -> System.out.println("Hello " + message);

したがって、names.foreach内部的に最初にconsumer.acceptメソッドを呼び出し、そのラムダ/匿名実装を実行して、SayHelloラムダ実装などを作成して呼び出します。ラムダ式を使用すると、コードは非常にコンパクトで明確です。あなたがそれがどのように機能するかを理解する必要がある唯一のこと、つまりあなたの場合、それはコンシューマーインターフェースを使用していた。

だから最終的な流れ: foreach -> consumer.accept -> sayhello.speak

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.