В чем разница между Callable <T> и поставщиком Java 8 <T>?

13

Я перешел на Java с C # после некоторых рекомендаций от CodeReview. Поэтому, когда я изучал LWJGL, я вспомнил одну вещь: каждый вызов Displayдолжен выполняться в том же потоке, в котором Display.create()был вызван метод. Помня об этом, я выбрал класс, который выглядит примерно так.

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

При написании этого класса вы заметите, что я создал метод с именем, isClosed()который возвращает Future<Boolean>. Это отправляет функции на мой Schedulerинтерфейс (который является не более чем оболочка вокруг ScheduledExecutorService. При написании scheduleметода на Schedulerя заметил , что я мог бы либо использовать Supplier<T>аргумент или в Callable<T>аргумент для представления функции, которая передается в. ScheduledExecutorServiceНе содержат переопределить, Supplier<T>но я заметил, что лямбда-выражение () -> Display.isCloseRequested()на самом деле совместимо с типами Callable<bool> и Supplier<bool> .

Мой вопрос заключается в том, есть ли разница между этими двумя, семантически или иным образом, и если да, то каково это, чтобы я мог придерживаться этого?

Дэн Кладовая
источник
У меня сложилось впечатление, что код, который не работает = ТАК, код, который работает, но нуждается в пересмотре = CodeReview, общие вопросы, которые могут или не могут нуждаться в коде = программисты. Мой код действительно работает и приведен только в качестве примера. Я не прошу пересмотра, просто спрашиваю о семантике.
Дэн Кладовая
..спросить о семантике чего-то не является концептуальным вопросом?
Дэн Кладовая
Я думаю, что это концептуальный вопрос, не такой концептуальный, как другие хорошие вопросы на этом сайте, но он не о реализации. Код работает, вопрос не в коде. Вопрос заключается в том, "какова разница между этими двумя интерфейсами?"
Почему вы хотите перейти с C # на Java!
Дидье А.
2
Есть одно отличие, а именно то, что Callable.call () генерирует исключения, а Supplier.get () - нет. Это делает последнее намного более привлекательным в лямбда-выражениях.
Торбьерн Равн Андерсен

Ответы:

6

Короткий ответ заключается в том, что оба используют функциональные интерфейсы, но также стоит отметить, что не все функциональные интерфейсы должны иметь @FunctionalInterfaceаннотацию. Критическая часть JavaDoc гласит:

Однако компилятор будет обрабатывать любой интерфейс, соответствующий определению функционального интерфейса, как функциональный интерфейс независимо от того, присутствует ли аннотация FunctionalInterface в объявлении интерфейса.

И самое простое определение функционального интерфейса (просто, без других исключений) просто:

Концептуально функциональный интерфейс имеет ровно один абстрактный метод.

Следовательно, в ответе @Maciej Chalapuk также можно отбросить аннотацию и указать желаемую лямбду:

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

Теперь, что делает оба Callableи Supplierфункциональные интерфейсы, потому что они содержат ровно один абстрактный метод:

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

Поскольку оба метода не принимают аргумента (в отличие от MyInterface.myCall(int)примера), формальные параметры пусты ( ()).

Я заметил, что лямбда-выражение () -> Display.isCloseRequested()на самом деле совместимо по типу с обоими Callable<Boolean> и Supplier<Boolean> .

Как вы уже поняли, это возможно потому, что оба абстрактных метода будут возвращать тип выражения, которое вы используете. Вы должны определенно использовать Callableданное использование ScheduledExecutorService.

Дальнейшее исследование (за рамками вопроса)

Оба интерфейса происходят из совершенно разных пакетов , поэтому они также используются по-разному. В вашем случае я не вижу, как Supplier<T>будет использоваться реализация, если она не предоставляет Callable:

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

Первое () ->можно свободно интерпретировать как « Supplierдает ...», а второе как « Callableдает ...». return value;это тело Callableлямбды, которое само является телом Supplierлямбды.

Однако, использование в этом надуманный пример получает немного сложно, так как теперь нужно get()из Supplierпервых , прежде чем get()-ting ваш результат от Future, который будет , в свою очередь call()СВОЙ Callableасинхронно.

public static <T> T doWork(Supplier<Callable<T>> callableSupplier) {
    // service being an instance of ExecutorService
    return service.submit(callableSupplier.get()).get();
}
ХИК
источник
1
Я переключаю принятый ответ на этот ответ, потому что он просто гораздо более всеобъемлющий
Дэн Пантри
Более длинное не означает более полезное, см. Ответ @ srrm_lwn.
SensorSmith
Ответ @SensorSmith srrms был оригинальным, который я пометил как принятый ответ. Я все еще думаю, что это более полезно.
Дэн Кладовая
21

Одно из основных различий между двумя интерфейсами состоит в том, что Callable позволяет создавать проверенные исключения изнутри реализации, в то время как Поставщик этого не делает.

Вот фрагменты кода из JDK, подчеркивающие это -

@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
источник
Это делает Callable непригодным в качестве аргумента в функциональных интерфейсах.
Basilevs
3
@Basilevs нет, это не так - его просто нельзя использовать в местах, которые ожидают Supplier, таких как потоковый API. Вы абсолютно можете передавать лямбды и ссылки на методы к методам, которые принимают Callable.
dimo414
12

Как вы заметили, на практике они делают одно и то же (предоставляют какую-то ценность), однако в принципе они предназначены для разных вещей:

A Callable- это « Задача, которая возвращает результат , а a Supplierпоставщик результатов ». Другими словами, a Callable- это способ ссылки на еще не выполненную единицу работы, в то время как a Supplier- это способ ссылки на еще неизвестное значение.

Возможно, что он Callableмог сделать очень мало работы и просто вернуть значение. Также возможно, что Supplierможно сделать довольно много работы (например, построить большую структуру данных). Но в целом то, что вас волнует, является их главной целью. Например, ExecutorServiceработает с Callables, потому что его основная цель - выполнить единицы работы. Хранилище данных с отложенной загрузкой будет использовать a Supplier, потому что оно заботится о предоставлении значения, без особого беспокойства о том, сколько работы может потребоваться.

Другой способ сформулировать различие состоит в том, что a Callableможет иметь побочные эффекты (например, запись в файл), тогда как a Supplierобычно не должно иметь побочных эффектов. В документации явно не упоминается об этом (поскольку это не является обязательным требованием ), но я бы посоветовал подумать об этом. Если работа идемпотентна, используйте a Supplier, если не используйте a Callable.

dimo414
источник
2

Оба являются обычными интерфейсами Java без специальной семантики. Callable является частью параллельного API. Поставщик является частью нового API функционального программирования. Они могут быть созданы из лямбда-выражений благодаря изменениям в Java8. @FunctionalInterface заставляет компилятор проверять работоспособность интерфейса и выдает ошибку, если это не так, но интерфейсу не требуется, чтобы эта аннотация была функциональным интерфейсом и была реализована с помощью лямбды. Это похоже на то, как метод может быть переопределен без пометки @Override, но не наоборот.

Вы можете определить свои собственные интерфейсы, совместимые с лямбдами, и задокументировать их с @FunctionalInterfaceаннотацией. Документирование необязательно, хотя.

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

...

MyInterface var = (int a) -> a != 0;
Мацей Халапук
источник
Хотя стоит отметить, что этот конкретный интерфейс называется IntPredicateв Java.
Конрад Боровски,