Qual é a diferença entre Callable <T> e Java 8's Supplier <T>?

13

Eu tenho mudado para Java a partir de C # depois de algumas recomendações de alguns no CodeReview. Portanto, quando eu estava pesquisando o LWJGL, lembrei-me de que toda chamada a Displayser executada no mesmo encadeamento em que o Display.create()método foi chamado. Lembrando disso, criei uma aula que se parece um pouco com isso.

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

Ao escrever esta classe, você notará que eu criei um método chamado isClosed()que retorna a Future<Boolean>. Isso envia uma função para minha Schedulerinterface (que nada mais é do que um invólucro em torno de um ScheduledExecutorService. Ao escrever o schedulemétodo no Scheduler, observei que eu poderia usar um Supplier<T>argumento ou um Callable<T>argumento para representar a função que é transmitida. ScheduledExecutorServiceNão continha um substituir por Supplier<T>mas notei que a expressão lambda () -> Display.isCloseRequested()é realmente do tipo compatível com ambos Callable<bool> e Supplier<bool> .

A minha pergunta é: existe uma diferença entre os dois, semanticamente ou de outra forma - e se sim, o que é, para que eu possa aderir?

Dan Pantry
fonte
Eu estava sob o código de impressão que não funciona = SO, código que funciona, mas precisa de revisão = CodeReview, perguntas gerais que podem ou não precisar de código = programadores. Meu código realmente funciona e está lá apenas como exemplo. Também não estou pedindo uma revisão, apenas perguntando sobre semântica.
Dan Pantry
..asking sobre a semântica de algo não é uma questão conceitual?
Dan Pantry
Eu acho que essa é uma pergunta conceitual, não tão conceitual quanto outras boas perguntas neste site, mas não é sobre implementação. O código funciona, a questão não é sobre o código. A questão é perguntar "qual é a diferença entre essas duas interfaces?"
Por que você gostaria de mudar de C # para Java!
Didier A.
2
Há uma diferença, a saber, que Callable.call () lança exceções e Supplier.get () não. Isso torna o último muito mais atraente nas expressões lambda.
Thorbjørn Ravn Andersen

Respostas:

6

A resposta curta é que ambos estão usando interfaces funcionais, mas também vale a pena notar que nem todas as interfaces funcionais devem ter a @FunctionalInterfaceanotação. A parte crítica do JavaDoc diz:

No entanto, o compilador tratará qualquer interface que atenda à definição de uma interface funcional como uma interface funcional, independentemente de uma anotação FunctionalInterface estar ou não presente na declaração da interface.

E a definição mais simples de uma interface funcional é (simplesmente, sem outras exclusões) apenas:

Conceitualmente, uma interface funcional possui exatamente um método abstrato.

Portanto, na resposta de @Maciej Chalapuk , também é possível descartar a anotação e especificar a lambda desejada:

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

Agora, o que cria interfaces funcionais Callablee Supplierfuncionais é que elas contêm exatamente um método abstrato:

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

Como os dois métodos não aceitam um argumento (em oposição ao MyInterface.myCall(int)exemplo), os parâmetros formais estão vazios ( ()).

Notei que a expressão lambda () -> Display.isCloseRequested()é realmente do tipo compatível com ambos Callable<Boolean> e Supplier<Boolean> .

Como você já deve poder inferir, isso ocorre apenas porque os dois métodos abstratos retornarão o tipo da expressão usada. Você definitivamente deveria usar um Callabledado seu uso de a ScheduledExecutorService.

Exploração adicional (além do escopo da pergunta)

Ambas as interfaces vêm de pacotes totalmente diferentes , portanto, também são usadas de maneira diferente. No seu caso, não vejo como uma implementação de será usada, a menos que esteja fornecendo um :Supplier<T>Callable

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

O primeiro () ->pode ser vagamente interpretado como "a Supplierdá ..." e o segundo como "a Callabledá ...". return value;é o corpo da Callablelambda, que em si é o corpo da Supplierlambda.

No entanto, o uso neste exemplo artificial fica um pouco complicado, pois agora você precisa, get()desde o Supplierprimeiro, antes de get()escrever o seu resultado no Future, o que, por call()sua vez, será Callableassíncrono.

public static <T> T doWork(Supplier<Callable<T>> callableSupplier) {
    // service being an instance of ExecutorService
    return service.submit(callableSupplier.get()).get();
}
hjk
fonte
1
Estou mudando resposta aceita a esta resposta, porque isso é simplesmente muito mais abrangente
Dan Pantry
Quanto mais tempo não equivale a mais útil, consulte a resposta de @ srrm_lwn.
precisa saber é o seguinte
A resposta do @SensorSmith srrms foi a original que marquei como resposta aceita. Eu ainda acho que este é mais útil.
Dan Pantry
21

Uma diferença básica entre as 2 interfaces é que Callable permite que exceções verificadas sejam lançadas na implementação, enquanto o Fornecedor não.

Aqui estão os trechos de código do JDK que destacam isso -

@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
fonte
Isso torna Callable inutilizável como argumento em interfaces funcionais.
Basilevs 28/06
3
@Basilevs, não, não é - não é utilizável em lugares que esperam Supplier, como a API de fluxo. Você absolutamente pode passar lambdas e referências de métodos a métodos que usam a Callable.
dimo414
12

Como você observa, na prática, eles fazem a mesma coisa (fornecem algum tipo de valor); no entanto, em princípio, eles pretendem fazer coisas diferentes:

A Callableé " Uma tarefa que retorna um resultado , enquanto a Supplieré" um fornecedor de resultados ". Em outras palavras, a Callableé uma maneira de referenciar uma unidade de trabalho ainda não executada, enquanto a Supplieré uma maneira de referenciar um valor ainda desconhecido.

É possível que um Callablepoderia fazer muito pouco trabalho e simplesmente retornar um valor. Também é possível Supplierque você possa fazer bastante trabalho (por exemplo, construir uma grande estrutura de dados). Mas, de um modo geral, o que lhe interessa é o principal objetivo. Por exemplo, um ExecutorServicefunciona com Callables, porque seu principal objetivo é executar unidades de trabalho. Um armazenamento de dados carregado preguiçosamente usaria umSupplier , porque ele se preocupa com o que está sendo fornecido um valor, sem muita preocupação com a quantidade de trabalho que pode demorar.

Outra maneira de expressar a distinção é que a Callablepode ter efeitos colaterais (por exemplo, gravar em um arquivo), enquanto a Suppliergeralmente deve ser livre de efeitos colaterais. A documentação não menciona explicitamente isso (já que não é um requisito ), mas eu sugiro pensar nesses termos. Se o trabalho for idempotente, use a Supplier, se não, use a Callable.

dimo414
fonte
2

Ambas são interfaces Java normais, sem semântica especial. Callable é uma parte da API simultânea. O fornecedor faz parte da nova API de programação funcional. Eles podem ser criados a partir de expressões lambda graças a alterações no Java8. @FunctionalInterface faz com que o compilador verifique se a interface está funcional e um erro se não estiver, mas uma interface não precisa dessa anotação para ser uma interface funcional e ser implementada por lambdas. É assim que um método pode ser uma substituição sem ser marcado como @Override, mas não vice-versa.

Você pode definir suas próprias interfaces compatíveis com lambdas e documentá-las com @FunctionalInterfaceanotação. A documentação é opcional.

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

...

MyInterface var = (int a) -> a != 0;
Maciej Chałapuk
fonte
Embora digno de nota, essa interface específica é chamada IntPredicateem Java.
319 Konrad Borowski