Was ist der Unterschied zwischen Callable <T> und Java 8s Supplier <T>?

13

Ich bin von C # auf Java umgestiegen, nachdem ich einige Empfehlungen von CodeReview erhalten hatte. Als ich mich mit LWJGL befasste, fiel mir ein, dass jeder Aufruf von Displayauf dem gleichen Thread ausgeführt werden muss, auf dem die Display.create()Methode aufgerufen wurde. Daran erinnernd, habe ich eine Klasse zusammengestellt, die ein bisschen so aussieht.

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

Beim Schreiben dieser Klasse werden Sie feststellen, dass ich eine Methode namens isClosed()a erstellt habe Future<Boolean>. Dies sendet eine Funktion an meine SchedulerSchnittstelle (die nichts anderes ist als ein Wrapper um ein ScheduledExecutorService. Beim Schreiben der scheduleMethode auf dem ist Schedulermir aufgefallen, dass ich entweder ein Supplier<T>Argument oder ein Callable<T>Argument verwenden kann, um die übergebene Funktion darzustellen. ScheduledExecutorServiceEnthält kein überschreiben, Supplier<T>aber ich habe festgestellt, dass der Lambda-Ausdruck () -> Display.isCloseRequested()tatsächlich mit beiden Callable<bool> und typkompatibel ist Supplier<bool>.

Meine Frage ist, gibt es einen Unterschied zwischen diesen beiden, semantisch oder anderweitig - und wenn ja, was ist das, damit ich mich daran halten kann?

Dan Pantry
quelle
Ich befand mich unter dem Impression-Code, der nicht funktioniert = SO, Code, der funktioniert, aber überprüft werden muss = CodeReview, allgemeine Fragen, die möglicherweise Code = Programmierer erfordern oder nicht. Mein Code funktioniert tatsächlich und ist nur als Beispiel da. Ich bitte auch nicht um eine Rezension, sondern nur um Semantik.
Dan Pantry
..über die Semantik von etwas zu fragen, ist keine konzeptionelle Frage?
Dan Pantry
Ich denke, dies ist eine konzeptionelle Frage, nicht so konzeptionell wie andere gute Fragen auf dieser Website, aber es geht nicht um die Implementierung. Der Code funktioniert, bei der Frage geht es nicht um den Code. Die Frage lautet: "Was ist der Unterschied zwischen diesen beiden Schnittstellen?"
Warum sollten Sie von C # auf Java umsteigen wollen?
Didier A.
2
Es gibt einen Unterschied, nämlich, dass Callable.call () Ausnahmen auslöst und Supplier.get () keine. Das macht letztere in Lambda-Ausdrücken viel attraktiver.
Thorbjørn Ravn Andersen

Antworten:

6

Die kurze Antwort ist, dass beide funktionale Schnittstellen verwenden, aber es ist auch erwähnenswert, dass nicht alle funktionalen Schnittstellen die @FunctionalInterfaceAnnotation haben müssen . Der kritische Teil des JavaDoc lautet:

Der Compiler behandelt jedoch jede Schnittstelle, die die Definition einer funktionalen Schnittstelle erfüllt, als funktionale Schnittstelle, unabhängig davon, ob in der Schnittstellendeklaration eine FunctionalInterface-Anmerkung vorhanden ist oder nicht.

Und die einfachste Definition einer funktionalen Schnittstelle ist (einfach, ohne andere Ausschlüsse) nur:

Konzeptionell hat eine funktionale Schnittstelle genau eine abstrakte Methode.

Daher ist es in der Antwort von @Maciej Chalapuk auch möglich, die Anmerkung zu löschen und das gewünschte Lambda anzugeben:

// 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

Was beide Callableund Supplierfunktionale Schnittstellen ausmacht , ist, dass sie genau eine abstrakte Methode enthalten:

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

Da beide Methoden (im Gegensatz zum MyInterface.myCall(int)Beispiel) kein Argument aufnehmen , sind die formalen Parameter leer ( ()).

Mir ist aufgefallen, dass der Lambda-Ausdruck () -> Display.isCloseRequested()tatsächlich mit beiden Callable<Boolean> und typkompatibel ist Supplier<Boolean>.

Wie Sie jetzt schließen können sollten, liegt das nur daran, dass beide abstrakten Methoden den Typ des von Ihnen verwendeten Ausdrucks zurückgeben. Sie sollten auf jeden Fall eine Callablebestimmte Verwendung von a verwenden ScheduledExecutorService.

Weitere Erkundung (über den Rahmen der Frage hinaus)

Beide Schnittstellen stammen aus völlig unterschiedlichen Paketen und werden daher auch unterschiedlich verwendet. In Ihrem Fall sehe ich nicht, wie eine Implementierung von Supplier<T>verwendet wird, es sei denn, sie liefert Folgendes Callable:

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

Das erste () ->kann lose als "a Suppliergibt ..." und das zweite als "a Callablegibt ..." interpretiert werden . return value;ist der Körper des CallableLambda, der selbst der Körper des SupplierLambda ist.

Allerdings Verwendung in diesem erfundenen Beispiel wird etwas kompliziert, wie Sie jetzt brauchen get()von den Supplierersten bevor get()Ihr Ergebnis aus der -ting Future, was wiederum call()Ihrem Callableasynchron.

public static <T> T doWork(Supplier<Callable<T>> callableSupplier) {
    // service being an instance of ExecutorService
    return service.submit(callableSupplier.get()).get();
}
hjk
quelle
1
Ich schalte akzeptierte Antwort auf diese Antwort, weil dies einfach viel umfassender ist
Dan Pantry
Längere entspricht nicht nützlicher, siehe Antwort von @ srrm_lwn.
SensorSmith
@ SensorSmith srrms Antwort war die ursprüngliche, die ich als akzeptierte Antwort markiert habe. Ich denke immer noch, dass dies nützlicher ist.
Dan Pantry
21

Ein grundlegender Unterschied zwischen den beiden Schnittstellen besteht darin, dass Callable das Auslösen von aktivierten Ausnahmen aus der Implementierung heraus ermöglicht, während dies bei Supplier nicht der Fall ist.

Hier sind die Codefragmente aus dem JDK, die dies hervorheben:

@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
quelle
Dies macht Callable als Argument in funktionalen Schnittstellen unbrauchbar.
Basilevs
3
@Basilevs nein, ist es nicht - es ist nur nicht an Orten verwendbar, die eine erwarten Supplier, wie zum Beispiel die Stream-API. Sie können Lambdas und Methodenreferenzen absolut an Methoden übergeben, die a nehmen Callable.
dimo414
12

Wie Sie bemerken, tun sie in der Praxis dasselbe (bieten einen Wert), aber im Prinzip sollen sie verschiedene Dinge tun:

A Callableist " Eine Aufgabe, die ein Ergebnis zurückgibt , während a Supplier" ein Ergebnislieferant "ist. Mit anderen Worten Callableist a eine Möglichkeit, auf eine noch nicht ausgeführte Arbeitseinheit Supplierzu verweisen , während a eine Möglichkeit ist, auf einen noch unbekannten Wert zu verweisen.

Es ist möglich, dass eine Callablesehr kleine Arbeit erledigt und einfach einen Wert zurückgibt. Es ist auch möglich, dass eine SupplierMenge Arbeit anfällt (z. B. Aufbau einer großen Datenstruktur). Aber im Allgemeinen ist das, worum es Ihnen bei beiden geht, ihr Hauptzweck. Zum Beispiel ExecutorServicearbeitet ein mit Callables, weil sein Hauptzweck darin besteht, Arbeitseinheiten auszuführen . Ein fauler belasteter Datenspeicher verwenden würde ein Supplier, denn es geht um kümmert wird geliefert Wert, ohne viel Sorge darüber , wie viel Arbeit , die nehmen könnte.

Eine andere Möglichkeit, die Unterscheidung zu formulieren, besteht darin, dass a CallableNebenwirkungen haben kann (z. B. Schreiben in eine Datei), während a Supplierim Allgemeinen nebenwirkungsfrei sein sollte. In der Dokumentation wird dies nicht explizit erwähnt (da dies nicht erforderlich ist ), aber ich würde vorschlagen, in diesen Begriffen zu denken. Wenn die Arbeit idempotent ist, benutze a Supplier, wenn nicht, benutze a Callable.

dimo414
quelle
2

Beides sind normale Java-Schnittstellen ohne spezielle Semantik. Callable ist Teil der gleichzeitigen API. Der Lieferant ist Teil der neuen funktionalen Programmierschnittstelle. Sie können dank Änderungen in Java8 aus Lambda-Ausdrücken erstellt werden. @FunctionalInterface veranlasst den Compiler, die Funktionsfähigkeit der Schnittstelle zu überprüfen, und gibt einen Fehler aus, wenn dies nicht der Fall ist. Eine Schnittstelle benötigt diese Annotation jedoch nicht, um eine funktionierende Schnittstelle zu sein und von Lambdas implementiert zu werden. So kann eine Methode ein Override sein, ohne als @Override markiert zu sein, aber nicht umgekehrt.

Sie können Ihre eigenen mit Lambdas kompatiblen Schnittstellen definieren und diese mit @FunctionalInterfaceAnmerkungen dokumentieren . Die Dokumentation ist jedoch optional.

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

...

MyInterface var = (int a) -> a != 0;
Maciej Chałapuk
quelle
Obwohl es erwähnenswert ist, wird diese spezielle Schnittstelle IntPredicatein Java aufgerufen .
Konrad Borowski