¿Cómo interactúan las llamadas lambda con las interfaces?

8

El fragmento de código que se muestra a continuación funciona. Sin embargo, no estoy seguro de por qué funciona. No estoy siguiendo la lógica de cómo la función lambda está pasando información a la interfaz.

¿Dónde se pasa el control? ¿Cómo tiene sentido el compilador de cada uno nen el bucle y cada uno messagecreado?

Este código compila y da los resultados esperados. Simplemente no estoy seguro de cómo.

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
fuente

Respuestas:

8

El SayHelloes una interfaz de método abstracto único que tiene un método que toma una cadena y devuelve vacío. Esto es análogo a un consumidor. Solo está proporcionando una implementación de ese método en forma de consumidor que es similar a la siguiente implementación anónima de clase interna.

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

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

Afortunadamente, su interfaz tiene un método único para que el sistema de tipos (más formalmente, el algoritmo de resolución de tipos) pueda inferir su tipo como SayHello. Pero si tuviera 2 o más métodos, esto no sería posible.

Sin embargo, un enfoque mucho mejor es declarar al consumidor antes del ciclo for y usarlo como se muestra a continuación. Declarar la implementación para cada iteración crea más objetos de los necesarios y me parece contra-intuitivo. Aquí está la versión mejorada usando referencias de métodos en lugar de lambda. La referencia del método acotado utilizada aquí llama al método relevante en la helloinstancia declarada anteriormente.

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

Actualizar

Dado que para lambdas apátridas que no capturan nada de su alcance léxico solo una vez que se creará una instancia, ambos enfoques simplemente crean una instancia de la SayHello, y no hay ninguna ganancia siguiendo el enfoque sugerido. Sin embargo, esto parece ser un detalle de implementación y no lo sabía hasta ahora. Por lo tanto, un enfoque mucho mejor es simplemente pasar a un consumidor a su ForEach como se sugiere en el comentario a continuación. También tenga en cuenta que todos estos enfoques crean solo una instancia de su SayHellointerfaz, mientras que la última es más sucinta. Así es como se ve.

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

Esta respuesta te dará más información sobre eso. Aquí está la sección relevante de JLS §15.27.4 : Evaluación en tiempo de ejecución de expresiones lambda

Estas reglas están destinadas a ofrecer flexibilidad a las implementaciones del lenguaje de programación Java, en el sentido de que:

  • No es necesario asignar un nuevo objeto en cada evaluación.

De hecho, inicialmente pensé que cada evaluación crea una nueva instancia, lo cual está mal. @ Holger, gracias por señalarlo, buena captura.

Ravindra Ranwala
fuente
Ok, creo que lo entiendo. Simplemente creé una versión de la interfaz de consumidor. Pero, como lo puse en el bucle, creé un objeto separado en cada bucle, consumiendo memoria. Mientras que, su implementación crea ese único objeto, y cambia "mensaje" en cada bucle. ¡Gracias por la explicación!
Brodiman
1
Sí, siempre es bueno si puede usar una interfaz funcional existente en lugar de implementar la suya propia. En este caso, puede usar en Consumer<String>lugar de tener una sayHellointerfaz.
Ravindra Ranwala
1
No importa dónde coloque la línea SayHello hello = message -> System.out.println("Hello " + message);, solo habrá una SayHelloinstancia. Dado que incluso ese objeto es obsoleto aquí de todos modos, ya que el mismo se puede lograr a través de names.forEach(n -> System.out.println("Hello " + n)), no hay mejora en su "versión mejorada".
Holger el
@ Holger He actualizado la respuesta con su sugerencia. Realmente lo aprecio. Entonces, ¿todas las implementaciones crean una única instancia de SayHello mientras que la sugerida por usted es más compacta? ¿No es así?
Ravindra Ranwala
1

La firma de foreach es algo así como a continuación:

void forEach(Consumer<? super T> action)

Toma en un objeto de consumidor. La interfaz de consumidor es una interfaz funcional (una interfaz con un único método abstracto). Acepta una entrada y no devuelve ningún resultado.

Aquí está la definición:

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

Por lo tanto, cualquier implementación, la de su caso, se puede escribir de dos maneras:

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

O

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

Del mismo modo, el código para la clase SayHello se puede escribir como

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

O

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

Entonces, names.foreachinternamente, primero llama al método consumer.accept y ejecuta su implementación lambda / anónima que crea y llama a la implementación SayHello lambda y así sucesivamente. Usando la expresión lambda, el código es muy compacto y claro. Lo único que necesita para comprender cómo funciona es decir, en su caso, estaba utilizando la interfaz de consumidor.

Entonces flujo final: foreach -> consumer.accept -> sayhello.speak

Nishant Lakhara
fuente