Como as chamadas lambda interagem com as Interfaces?

8

O trecho de código mostrado abaixo funciona. No entanto, não sei por que isso funciona. Não estou seguindo a lógica de como a função lambda está passando informações para a interface.

Onde o controle está sendo passado? Como o compilador está entendendo cada um nno loop e cada um deles messagecriado?

Esse código compila e fornece os resultados esperados. Só não sei como.

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);
    }
}
Brodiman
fonte

Respostas:

8

A SayHelloé uma interface Single Abstract Method que possui um método que pega uma string e retorna nulo. Isso é análogo a um consumidor. Você está apenas fornecendo uma implementação desse método na forma de um consumidor semelhante à seguinte implementação de classe interna anônima.

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

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

Felizmente, sua interface possui um método único para que o sistema de tipos (mais formalmente, o algoritmo de resolução de tipos) possa inferir seu tipo como SayHello. Mas se tivesse dois ou mais métodos, isso não seria possível.

No entanto, uma abordagem muito melhor é declarar o consumidor antes do loop for e usá-lo como mostrado abaixo. Declarar a implementação para cada iteração cria mais objetos do que o necessário e parece contra-intuitivo para mim. Aqui está a versão aprimorada usando referências de método em vez de lambda. A referência do método limitado usada aqui chama o método relevante na helloinstância declarada acima.

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

Atualizar

Dado que para lambdas sem estado que não captura nada do seu escopo lexical apenas uma vez que a instância é criada, ambas as abordagens simplesmente criam uma instância da SayHelloe não há nenhum ganho após a abordagem sugerida. No entanto, este parece ser um detalhe de implementação e eu não o conhecia até agora. Portanto, uma abordagem muito melhor é passar o consumidor para o seu forEach, conforme sugerido no comentário abaixo. Observe também que todas essas abordagens criam apenas uma instância da sua SayHellointerface, enquanto a última é mais sucinta. Aqui está como fica.

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

Esta resposta lhe dará mais informações sobre isso. Aqui está a seção relevante do JLS §15.27.4 : Avaliação em tempo de execução de expressões lambda

Essas regras visam oferecer flexibilidade às implementações da linguagem de programação Java, na medida em que:

  • Um novo objeto não precisa ser alocado em todas as avaliações.

De fato, inicialmente pensei que toda avaliação cria uma nova instância, o que está errado. @ Holger obrigado por apontar isso, boa captura.

Ravindra Ranwala
fonte
Ok, acho que entendi. Eu simplesmente criei uma versão da interface do consumidor. Mas, desde que o coloquei no loop, criei um objeto separado em cada loop, consumindo memória. Visto que sua implementação cria esse objeto e troca a "mensagem" em cada loop. Obrigado pela explicação!
Brodiman 03/11/19
1
Sim, é sempre bom se você pode usar uma interface funcional existente em vez de implementar a sua própria. Nesse caso, você pode usar em Consumer<String>vez de ter sayHellointerface.
Ravindra Ranwala 4/11/19
1
Não importa onde você coloca a linha SayHello hello = message -> System.out.println("Hello " + message);, haverá apenas uma SayHelloinstância. Dado que mesmo um objeto é obsoleto aqui de qualquer maneira, como o mesmo é possível via names.forEach(n -> System.out.println("Hello " + n)), não há melhorias na sua "versão aprimorada".
Holger
@ Holger Atualizei a resposta com sua sugestão. Realmente aprecio isso. Então, todas as implementações criam uma única instância do SayHello, enquanto a sugerida por você é mais compacta? Não é assim?
Ravindra Ranwala 5/11/19
1

A assinatura do foreach é semelhante à abaixo:

void forEach(Consumer<? super T> action)

Leva em um objeto de consumidor. A interface do consumidor é uma interface funcional (uma interface com um único método abstrato). Ele aceita uma entrada e não retorna nenhum resultado.

Aqui está a definição:

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

Portanto, qualquer implementação, a do seu caso, pode ser escrita de duas maneiras:

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

OU

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

Da mesma forma, o código da classe SayHello pode ser escrito como

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

OU

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

Portanto, names.foreachprimeiro internamente chama o método consumer.accept e executa sua implementação lambda / anônima, que cria e chama a implementação SayHello lambda e assim por diante. Usando a expressão lambda, o código é muito compacto e claro. A única coisa que você precisa entender é como está funcionando, ou seja, no seu caso, ele estava usando a interface do Consumidor.

Fluxo final: foreach -> consumer.accept -> sayhello.speak

Nishant Lakhara
fonte