Jaka jest różnica między wywoływalnym <T> a dostawcą Java 8 <T>?

14

Przełączam się na Javę z C # po kilku zaleceniach z CodeReview. Tak więc, kiedy patrzyłem na LWJGL, przypomniałem sobie, że każde wywołanie Displaymusi być wykonane w tym samym wątku, w którym wywołano Display.create()metodę. Pamiętając o tym, przygotowałem klasę, która wygląda trochę tak.

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

Pisząc tę ​​klasę zauważysz, że stworzyłem metodę o nazwie, isClosed()która zwraca a Future<Boolean>. To wywołuje funkcyjne do mojego Schedulerinterfejsu (który nie jest niczym więcej niż owinięcie wokół ScheduledExecutorService. Pisząc ten schedulesposób na SchedulerZauważyłem, że mogę albo użyć Supplier<T>argumentu lub do Callable<T>argumentu do reprezentowania funkcji, która jest przekazywana w. ScheduledExecutorServiceNie zawierał przesłonięcie, Supplier<T>ale zauważyłem, że wyrażenie lambda () -> Display.isCloseRequested()jest w rzeczywistości zgodne z typem zarówno z, jak Callable<bool> i Supplier<bool> .

Moje pytanie brzmi: czy istnieje różnica między tymi dwoma, semantycznie czy inaczej - a jeśli tak, to co to jest, żebym mógł się do tego zastosować?

Dan Pantry
źródło
Byłem pod wrażeniem kodu, który nie działa = SO, kod, który działa, ale wymaga przeglądu = CodeReview, ogólne pytania, które mogą, ale nie muszą potrzebować kodu = programiści. Mój kod faktycznie działa i jest tam tylko jako przykład. Nie proszę też o recenzję, tylko o semantykę.
Dan Pantry
... zadawanie pytań o semantykę czegoś nie jest pytaniem koncepcyjnym?
Dan Pantry
Myślę, że jest to pytanie koncepcyjne, nie tak koncepcyjne jak inne dobre pytania na tej stronie, ale nie dotyczy implementacji. Kod działa, pytanie nie dotyczy kodu. Pytanie brzmi „jaka jest różnica między tymi dwoma interfejsami?”
Dlaczego chcesz przejść z C # na Javę!
Didier A.
2
Jest jedna różnica, a mianowicie, że Callable.call () zgłasza wyjątki, a dostawca.get () nie. To sprawia, że ​​ten ostatni jest znacznie bardziej atrakcyjny w wyrażeniach lambda.
Thorbjørn Ravn Andersen

Odpowiedzi:

6

Krótka odpowiedź jest taka, że ​​oba używają funkcjonalnych interfejsów, ale warto również zauważyć, że nie wszystkie funkcjonalne interfejsy muszą mieć @FunctionalInterfaceadnotację. Krytyczna część JavaDoc brzmi:

Jednak kompilator będzie traktował każdy interfejs spełniający definicję interfejsu funkcjonalnego jako interfejs funkcjonalny, niezależnie od tego, czy adnotacja FunctionalInterface znajduje się w deklaracji interfejsu.

A najprostsza definicja interfejsu funkcjonalnego to (po prostu, bez innych wyjątków):

Pod względem koncepcyjnym funkcjonalny interfejs ma dokładnie jedną abstrakcyjną metodę.

Dlatego w odpowiedzi @Maciej Chalapuk można również upuścić adnotację i podać żądaną lambda:

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

Teraz interfejsy funkcjonalne Callablei Supplierfunkcjonalne sprawiają, że zawierają one dokładnie jedną abstrakcyjną metodę:

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

Ponieważ obie metody nie przyjmują argumentu (w przeciwieństwie do MyInterface.myCall(int)przykładu), parametry formalne są puste ( ()).

Zauważyłem, że wyrażenie lambda () -> Display.isCloseRequested()jest w rzeczywistości kompatybilne z obydwoma Callable<Boolean> i Supplier<Boolean> .

Jak powinieneś być w stanie wnioskować, to dlatego, że obie metody abstrakcyjne zwrócą typ używanego wyrażenia. Zdecydowanie powinieneś korzystać z Callabledanego użycia ScheduledExecutorService.

Dalsza eksploracja (poza zakresem pytania)

Oba interfejsy pochodzą z zupełnie różnych pakietów , dlatego też są używane inaczej. W twoim przypadku nie widzę sposobu Supplier<T>użycia implementacji , chyba że zapewnia ona Callable:

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

Pierwszy () ->można luźno interpretować jako „ Supplierdaje…”, a drugi jako „ Callabledaje…”. return value;jest ciałem Callablelambda, które samo jest ciałem Supplierlambda.

Jednak użycie w tym wymyślonym przykładzie staje się nieco skomplikowane, ponieważ teraz musisz to zrobić get()od Supplierpierwszego, zanim get()odrzucisz wynik z Future, co z kolei spowoduje, call()że Callableasynchronicznie.

public static <T> T doWork(Supplier<Callable<T>> callableSupplier) {
    // service being an instance of ExecutorService
    return service.submit(callableSupplier.get()).get();
}
hjk
źródło
1
Zmieniam zaakceptowaną odpowiedź na tę odpowiedź, ponieważ jest ona po prostu o wiele bardziej wyczerpująca
Dan Pantry
Dłuższe nie oznacza bardziej przydatne, patrz odpowiedź @ srrm_lwn.
SensorSmith
Odpowiedź @SensorSmith srrms była oryginalna i oznaczona jako odpowiedź zaakceptowana. Nadal uważam, że ten jest bardziej przydatny.
Dan Pantry
21

Jedną podstawową różnicą między dwoma interfejsami jest to, że Callable pozwala na rzucanie sprawdzonych wyjątków z poziomu jego implementacji, podczas gdy Dostawca nie.

Oto fragmenty kodu z JDK podkreślające to -

@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
źródło
To sprawia, że ​​Callable jest bezużyteczny jako argument w interfejsach funkcjonalnych.
Basilevs,
3
@Basilevs nie, nie robi - nie jest po prostu użyteczny w miejscach, które oczekują Supplier, takich jak interfejs API strumienia. Absolutnie możesz przekazać lambdas i referencje metod do metod, które wymagają Callable.
dimo414,
13

Jak zauważasz, w praktyce robią to samo (zapewniają pewną wartość), jednak w zasadzie mają na celu różne rzeczy:

A Callableto „ Zadanie, które zwraca wynik , podczas gdy a Supplierjest„ dostawcą wyników ”. Innymi słowy, a Callablejest sposobem na odniesienie się do jeszcze niewykonanej jednostki pracy, a a Supplierjest sposobem na odniesienie do jeszcze nieznanej wartości.

Możliwe, że Callablewykona bardzo mało pracy i po prostu zwróci wartość. Możliwe jest również, że Suppliermoże wykonać sporo pracy (np. Zbudować dużą strukturę danych). Ale ogólnie mówiąc, tym, na czym ci zależy, jest ich główny cel. Na przykład an ExecutorServicedziała z Callables, ponieważ jego głównym celem jest wykonywanie jednostek pracy. Leniwie załadowany magazyn danych skorzystałby z Supplier, ponieważ zależy mu na dostarczeniu wartości, bez większego obawy o to, ile pracy może to zająć.

Innym sposobem sformułowania tego rozróżnienia jest to, że a Callablemoże mieć skutki uboczne (np. Zapis do pliku), podczas gdy a Supplierogólnie powinno być wolne od skutków ubocznych. Dokumentacja wyraźnie nie wspomina o tym (ponieważ nie jest to wymóg ), ale sugerowałbym myślenie w tych kategoriach. Jeśli praca jest idempotentna, użyj a Supplier, jeśli nie, użyj Callable.

dimo414
źródło
2

Oba są normalnymi interfejsami Java bez specjalnej semantyki. Callable jest częścią współbieżnego API. Dostawca jest częścią nowego funkcjonalnego API programowania. Można je tworzyć z wyrażeń lambda dzięki zmianom w Javie8. @Funkcjonalny interfejs sprawia, że ​​kompilator sprawdza, czy interfejs działa, i generuje błąd, jeśli tak nie jest, ale interfejs nie potrzebuje tej adnotacji, aby był interfejsem funkcjonalnym i musi zostać zaimplementowany przez lambdas. To tak, jak metoda może być zastąpieniem bez oznaczenia @Override, ale nie odwrotnie.

Możesz zdefiniować własne interfejsy kompatybilne z lambdami i dokumentować je za pomocą @FunctionalInterfaceadnotacji. Dokumentowanie jest jednak opcjonalne.

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

...

MyInterface var = (int a) -> a != 0;
Maciej Chałapuk
źródło
Chociaż warto zauważyć, ten konkretny interfejs nazywa się IntPredicatew Javie.
Konrad Borowski