¿Cuál es la diferencia entre Callable <T> y Java 8's Supplier <T>?

13

He estado cambiando a Java desde C # después de algunas recomendaciones de algunos en CodeReview. Entonces, cuando estaba buscando en LWJGL, una cosa que recordaba era que cada llamada a Displaydebe ejecutarse en el mismo hilo en el que Display.create()se invocó el método. Recordando esto, preparé una clase que se parece un poco a esto.

public class LwjglDisplayWindow implements DisplayWindow {
    private final static int TargetFramesPerSecond = 60;
    private final Scheduler _scheduler;

    public LwjglDisplayWindow(Scheduler displayScheduler, DisplayMode displayMode) throws LWJGLException {
        _scheduler = displayScheduler;
        Display.setDisplayMode(displayMode);
        Display.create();
    }

    public void dispose() {
        Display.destroy();
    }

    @Override
    public int getTargetFramesPerSecond() { return TargetFramesPerSecond; }

    @Override
    public Future<Boolean> isClosed() {
        return _scheduler.schedule(() -> Display.isCloseRequested());
    }
}

Mientras escribe esta clase, notará que creé un método llamado isClosed()que devuelve a Future<Boolean>. Esto envía una función a mi Schedulerinterfaz (que no es más que un contenedor alrededor de un ScheduledExecutorService. Al escribir el schedulemétodo en el Schedulernoté que podía usar un Supplier<T>argumento o un Callable<T>argumento para representar la función que se pasa. ScheduledExecutorServiceNo contenía un anule Supplier<T>pero, noté que la expresión lambda () -> Display.isCloseRequested()es de tipo compatible con ambos Callable<bool> y Supplier<bool> .

Mi pregunta es, ¿hay alguna diferencia entre esos dos, semánticamente o de otro modo? Y, de ser así, ¿cuál es, para que pueda adherirme?

Dan Pantry
fuente
Estaba bajo el código de impresión que no funciona = SO, código que funciona pero necesita revisión = CodeReview, preguntas generales que pueden o no necesitar código = programadores. Mi código realmente funciona y solo está ahí como ejemplo. Tampoco estoy pidiendo una revisión, solo preguntando sobre semántica.
Dan Pantry
... preguntar sobre la semántica de algo no es una pregunta conceptual.
Dan Pantry
Creo que esta es una pregunta conceptual, no tan conceptual como otras buenas preguntas en este sitio, pero no se trata de implementación. El código funciona, la pregunta no es sobre el código. La pregunta es "¿cuál es la diferencia entre estas dos interfaces?"
¿Por qué querrías cambiar de C # a Java?
Didier A.
2
Hay una diferencia, a saber, que Callable.call () arroja excepciones y Supplier.get () no. Eso hace que este último sea mucho más atractivo en las expresiones lambda.
Thorbjørn Ravn Andersen

Respuestas:

6

La respuesta corta es que ambos están utilizando interfaces funcionales, pero también vale la pena señalar que no todas las interfaces funcionales deben tener la @FunctionalInterfaceanotación. La parte crítica de JavaDoc dice:

Sin embargo, el compilador tratará cualquier interfaz que cumpla con la definición de una interfaz funcional como una interfaz funcional, independientemente de si una anotación FunctionalInterface está presente o no en la declaración de la interfaz.

Y la definición más simple de una interfaz funcional es (simplemente, sin otras exclusiones) solo:

Conceptualmente, una interfaz funcional tiene exactamente un método abstracto.

Por lo tanto, en la respuesta de @Maciej Chalapuk , también es posible eliminar la anotación y especificar la lambda deseada:

// interface
public interface MyInterface {
    boolean myCall(int arg);
}

// method call
public boolean invokeMyCall(MyInterface arg) {
    return arg.myCall(0);
}

// usage
instance.invokeMyCall(a -> a != 0); // returns true if the argument supplied is not 0

Ahora, lo que hace que ambos Callabley Supplierfuncionales de interfaces se debe a que contienen exactamente un método abstracto:

  • Callable.call()
  • Supplier.get()

Como ambos métodos no toman en cuenta un argumento (a diferencia del MyInterface.myCall(int)ejemplo), los parámetros formales están vacíos ( ()).

Me di cuenta de que la expresión lambda () -> Display.isCloseRequested()es de tipo compatible con ambos Callable<Boolean> y Supplier<Boolean> .

Como ya debería poder inferir, eso se debe a que ambos métodos abstractos devolverán el tipo de expresión que usa. Definitivamente deberías estar usando un Callabledado tu uso de a ScheduledExecutorService.

Exploración adicional (más allá del alcance de la pregunta)

Ambas interfaces provienen de paquetes completamente diferentes , por lo tanto, también se usan de manera diferente. En su caso, no veo cómo se utilizará una implementación de , a menos que proporcione un :Supplier<T>Callable

public static <T> Supplier<Callable<T>> getCallable(T value) {
    return () -> () -> {
        return value;
    };
}

El primero () ->se puede interpretar libremente como "un Supplierda ..." y el segundo como "un Callableda ...". return value;es el cuerpo de la Callablelambda, que es el cuerpo de la Supplierlambda.

Sin embargo, el uso en este ejemplo artificial se vuelve un poco complicado, ya que ahora debe hacerlo get()desde el Supplierprimero antes de get()obtener su resultado desde el Future, que a call()su vez será Callableasincrónico.

public static <T> T doWork(Supplier<Callable<T>> callableSupplier) {
    // service being an instance of ExecutorService
    return service.submit(callableSupplier.get()).get();
}
hjk
fuente
1
Estoy cambiando la respuesta aceptada a esta respuesta porque esto es simplemente mucho más completo
Dan Pantry
Ya no equivale a más útil, vea la respuesta de @ srrm_lwn.
SensorSmith
La respuesta de @SensorSmith srrms fue la original que marqué como la respuesta aceptada. Todavía creo que este es más útil.
Dan Pantry
21

Una diferencia básica entre las 2 interfaces es que Callable permite que se lancen excepciones controladas desde su implementación, mientras que el Proveedor no.

Aquí están los fragmentos de código del JDK que destacan esto:

@FunctionalInterface
public interface Callable<V> {
/**
 * Computes a result, or throws an exception if unable to do so.
 *
 * @return computed result
 * @throws Exception if unable to compute a result
 */
V call() throws Exception;
}

@FunctionalInterface
public interface Supplier<T> {

/**
 * Gets a result.
 *
 * @return a result
 */
T get();
}
srrm_lwn
fuente
Esto hace que Callable sea inutilizable como argumento en las interfaces funcionales.
Basilevs
3
@Basilevs no, no lo hace, simplemente no se puede usar en lugares que esperan una Supplier, como la API de transmisión. Absolutamente puede pasar lambdas y referencias de métodos a métodos que toman un Callable.
dimo414
12

Como observa, en la práctica hacen lo mismo (proporcionan algún tipo de valor), sin embargo, en principio, tienen la intención de hacer cosas diferentes:

A Callablees " Una tarea que devuelve un resultado , mientras que a Supplieres" un proveedor de resultados ". En otras palabras, a Callablees una forma de hacer referencia a una unidad de trabajo aún no ejecutada, mientras que a Supplieres una forma de hacer referencia a un valor aún desconocido.

Es posible que a Callablepueda hacer muy poco trabajo y simplemente devolver un valor. También es posible que Supplierpueda hacer mucho trabajo (por ejemplo, construir una estructura de datos grande). Pero, en general, lo que te importa es su propósito principal. Por ejemplo, un ExecutorServicefunciona con Callables, porque su propósito principal es ejecutar unidades de trabajo. Un almacén de datos con carga lenta usaría a Supplier, porque le importa que se le proporcione un valor, sin preocuparse por la cantidad de trabajo que podría tomar.

Otra forma de expresar la distinción es que a Callablepuede tener efectos secundarios (por ejemplo, escribir en un archivo), mientras Supplierque generalmente no debería tener efectos secundarios. La documentación no menciona explícitamente esto (ya que no es un requisito ), pero sugeriría pensar en esos términos. Si el trabajo es idempotente, use a Supplier, si no, use a Callable.

dimo414
fuente
2

Ambas son interfaces Java normales sin semántica especial. Callable es parte de la API concurrente. El proveedor es parte de la nueva API de programación funcional. Se pueden crear a partir de expresiones lambda gracias a los cambios en Java8. @FunctionalInterface hace que el compilador compruebe que la interfaz es funcional y genera un error si no lo es, pero una interfaz no necesita esa anotación para ser una interfaz funcional e implementada por lambdas. Así es como un método puede ser una anulación sin estar marcado @Override pero no al revés.

Puede definir sus propias interfaces compatibles con lambdas y documentarlas con @FunctionalInterfaceanotaciones. Sin embargo, la documentación es opcional.

@FunctionalInterface
public interface MyInterface {
    boolean myCall(int arg);
}

...

MyInterface var = (int a) -> a != 0;
Maciej Chałapuk
fuente
Aunque vale la pena señalar que esta interfaz en particular se llama IntPredicateen Java.
Konrad Borowski