Как лямбда-вызовы взаимодействуют с интерфейсами? - PullRequest
7 голосов
/ 03 ноября 2019

Фрагмент кода, показанный ниже, работает. Однако я не уверен, почему это работает. Я не совсем следую логике того, как лямбда-функция передает информацию в интерфейс.

Где передается контроль? Каков смысл в компиляторе каждого 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);
    }
}

Ответы [ 2 ]

8 голосов
/ 03 ноября 2019

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);

Обновление

Учитывая, что для лямбд без сохранения состояния это не захватываетчто-либо из их лексического контекста только один раз когда-либо будет создано, оба подхода просто создают один экземпляр SayHello, и после предложенного подхода нет никакой выгоды. Однако это, кажется, деталь реализации, и я не знал об этом до сих пор. Так что гораздо лучший подход - просто передать потребителю свой forEach, как это предлагается в комментарии ниже. Также обратите внимание, что все эти подходы создают только один экземпляр вашего SayHello интерфейса, в то время как последний более лаконичен. Вот как это выглядит.

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

Этот ответ даст вам более полное представление об этом. Вот соответствующий раздел из JLS §15.27.4 : Оценка лямбда-выражений во время выполнения

Эти правила предназначены для обеспечения гибкости для реализаций языка программирования Java, вчто:

  • При каждой оценке не требуется выделять новый объект.

На самом деле, я изначально думал, что каждая оценка создает новый экземпляр, что неправильно,@ Хольгер, спасибо за указание, хороший улов.

1 голос
/ 04 ноября 2019

Подпись foreach выглядит примерно так:

void forEach(Consumer<? super T> action)

Она принимает объект Потребителя. Интерфейс Consumer - это функциональный интерфейс (интерфейс с одним абстрактным методом). Он принимает входные данные и не возвращает результата.

Вот определение:

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

Следовательно, любая реализация, которая в вашем случае может быть записана двумя способами:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...