O Java 8 chegou, é hora de aprender algo novo. Java 7 e Java 6 são versões apenas ligeiramente modificadas, mas Java 8 terá grandes melhorias. Talvez o Java 8 seja muito grande? Hoje darei a vocês uma explicação completa da nova abstração CompletableFuture no JDK 8. Como todos sabemos, o Java 8 será lançado em menos de um ano, portanto este artigo é baseado no JDK 8 build 88 com suporte lambda. CompletableFuture estende Future fornece métodos, operadores unários e promove assincronicidade e um modelo de programação orientado a eventos que não para nas versões mais antigas do Java. Se você abrir o JavaDoc do CompletableFuture, ficará chocado. Existem cerca de cinquenta métodos (!), e alguns deles são muito interessantes e difíceis de entender, por exemplo:
Copie o código da seguinte maneira: public <U,V> CompletableFuture<V> thenCombineAsync(
CompletableFuture<? estende U> outro,
BiFunção<? super T,? super U,?
Executor executor)
Não se preocupe, continue lendo. CompletableFuture coleta todas as características de ListenableFuture em Guava e SettableFuture. Além disso, as expressões lambda integradas aproximam-no dos futuros Scala/Akka. Isso pode parecer bom demais para ser verdade, mas continue lendo. CompletableFuture tem dois aspectos principais que são superiores ao retorno de chamada/conversão assíncrona do Future em ol, que permite que o valor de CompletableFuture seja definido a partir de qualquer thread a qualquer momento.
1. Extraia e modifique o valor do pacote
Freqüentemente, os futuros representam código em execução em outros threads, mas nem sempre é esse o caso. Às vezes você deseja criar um Future para indicar que sabe o que acontecerá, como a chegada de uma mensagem JMS. Então você tem um futuro, mas nenhum trabalho assíncrono potencial no futuro. Você simplesmente deseja que seja concluído (resolvido) quando uma mensagem JMS futura chegar, que é acionada por um evento. Nesse caso, você pode simplesmente criar um CompletableFuture para retornar ao seu cliente, e simplesmente complete() irá desbloquear todos os clientes que aguardam o Future enquanto você achar que seu resultado está disponível.
Primeiro você pode simplesmente criar um novo CompletableFuture e entregá-lo ao seu cliente:
Copie o código da seguinte forma: public CompletableFuture<String> ask() {
final CompletableFuture<String> future = new CompletableFuture<>();
//...
retornar futuro;
}
Observe que este futuro não tem conexão com Callable, não há pool de threads e não funciona de forma assíncrona. Se o código do cliente agora chamar ask().get() ele será bloqueado para sempre. Se os registradores completarem o retorno de chamada, eles nunca terão efeito. Então, qual é a chave? Agora você pode dizer:
Copie o código da seguinte forma: future.complete("42")
...Neste momento, todos os clientes Future.get() obterão o resultado da string, e ela entrará em vigor imediatamente após a conclusão do retorno de chamada. Isso é muito conveniente quando você deseja representar a tarefa de um Future e não há necessidade de calcular a tarefa de algum thread de execução. CompletableFuture.complete() só pode ser chamada uma vez, as chamadas subsequentes serão ignoradas. Mas também existe um backdoor chamado CompletableFuture.obtrudeValue(...) que substitui o valor anterior de um novo Future, portanto, use-o com cuidado.
Às vezes você quer ver o que acontece quando um sinal falha, pois você sabe que um objeto Future pode lidar com o resultado ou exceção que ele contém. Se quiser passar algumas exceções ainda mais, você pode usar CompletableFuture.completeExceptionally(ex) (ou usar um método mais poderoso como obtrudeException(ex) para substituir a exceção anterior). completeExceptionally() também desbloqueia todos os clientes em espera, mas desta vez lança uma exceção de get(). Falando em get(), há também o método CompletableFuture.join() com mudanças sutis no tratamento de erros. Mas no geral, eles são todos iguais. Finalmente, existe o método CompletableFuture.getNow(valueIfAbsent) que não bloqueia, mas retornará o valor padrão se o Future ainda não tiver sido concluído, o que o torna muito útil na construção de sistemas robustos onde não queremos esperar muito.
O método estático final é usarcompleteFuture(value) para retornar o objeto Future concluído, o que pode ser muito útil ao testar ou escrever algumas camadas do adaptador.
2. Crie e obtenha CompletableFuture
Ok, então criar um CompletableFuture manualmente é nossa única opção? incerto. Assim como os Futures regulares, podemos associar tarefas existentes, e CompletableFuture usa métodos de fábrica:
Copie o código do código da seguinte forma:
static <U> CompletableFuture<U> supplyAsync(Supplier<U> fornecedor);
static <U> CompletableFuture<U> supplyAsync(Fornecedor<U> fornecedor, Executor executor);
static CompletableFuture<Void> runAsync(Runnable runnable);
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
O método sem parâmetros Executor termina com...Async e usará ForkJoinPool.commonPool() (pool comum e global introduzido no JDK8), que se aplica à maioria dos métodos da classe CompletableFuture. runAsync() é fácil de entender, observe que ele requer um Runnable, portanto retorna CompletableFuture<Void> pois Runnable não retorna nenhum valor. Se você precisar lidar com operações assíncronas e retornar resultados, use Supplier<U>:
Copie o código do código da seguinte forma:
final CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Substituir
string pública get() {
//...longa duração...
retornar "42";
}
}, executor);
Mas não se esqueça, existem expressões lambdas no Java 8!
Copie o código do código da seguinte forma:
finalCompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
//...longa duração...
retornar "42";
}, executor);
ou:
Copie o código do código da seguinte forma:
final CompletableFuture<String> futuro =
CompletableFuture.supplyAsync(() -> longRunningTask(params), executor);
Embora este artigo não seja sobre lambdas, uso expressões lambda com bastante frequência.
3. Conversão e ação em CompletableFuture(thenApply)
Eu disse que CompletableFuture é melhor que Future, mas você não sabe por quê? Simplificando, porque CompletableFuture é um átomo e um fator. O que eu disse não foi útil? Tanto Scala quanto JavaScript permitem registrar um retorno de chamada assíncrono quando um futuro é concluído, e não precisamos esperar e bloqueá-lo até que esteja pronto. Podemos simplesmente dizer: quando você executa esta função, o resultado aparece. Além disso, podemos empilhar essas funções, combinar vários futuros, etc. Por exemplo, se convertermos de String para Integer, podemos converter de CompletableFuture para CompletableFuture<Integer sem associação. Isso é feito via thenApply():
Copie o código do código da seguinte forma:
<U> CompletableFuture<U> thenApply(Function<? super T,? estende U> fn);
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? estende U> fn);
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor);<p></p>
<p>Como mencionado... a versão Async fornece a maioria das operações no CompletableFuture, então irei ignorá-las nas seções posteriores. Lembre-se, o primeiro método chamará o método no mesmo thread em que o futuro for concluído, enquanto os dois restantes o chamarão de forma assíncrona em diferentes pools de threads.
Vamos dar uma olhada no fluxo de trabalho de thenApply():</p>
<p><pré>
CompletableFuture<String> f1 = //...
CompletableFuture<Integer> f2 = f1.thenApply(Integer::parseInt);
CompletableFuture<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</p>
Ou em uma declaração:
Copie o código do código da seguinte forma:
CompletableFuturo<Duplo> f3 =
f1.thenApply(Integer::parseInt).thenApply(r -> r * r * Math.PI);
Aqui, você verá a conversão de uma sequência, de String para Inteiro para Duplo. Mas o mais importante é que essas transformações não são executadas imediatamente nem param. Essas transformações não são executadas imediatamente nem param. Eles simplesmente se lembram do programa que executaram quando o f1 original foi concluído. Se determinadas transformações consumirem muito tempo, você poderá fornecer seu próprio Executor para executá-las de forma assíncrona. Observe que esta operação é equivalente a um mapa unário em Scala.
4. Execute o código concluído (thenAccept/thenRun)
Copie o código do código da seguinte forma:
CompletableFuture<Void> thenAccept(Consumer<? super T> bloco);
CompletableFuture<Void> thenRun(ação executável);
Existem dois métodos típicos de estágio "final" em pipelines futuros. Eles são preparados quando você usa o valor do futuro. Quando thenAccept() fornece o valor final, thenRun executa o Runnable, que nem sequer tem como calcular o valor. Por exemplo:
Copie o código do código da seguinte forma:
future.thenAcceptAsync(dbl -> log.debug("Resultado: {}", dbl), executor);
log.debug("Continuando");
...Variáveis assíncronas também estão disponíveis de duas maneiras, executores implícitos e explícitos, e não vou enfatizar muito esse método.
Os métodos thenAccept()/thenRun() não bloqueiam (mesmo se não houver nenhum executor explícito). Eles são como um ouvinte/manipulador de eventos, que será executado por um período de tempo quando você o conectar a um futuro. A mensagem “Continuando” aparecerá imediatamente, embora o futuro nem esteja concluído.
5. Tratamento de erros de um único CompletableFuture
Até agora, discutimos apenas os resultados dos cálculos. E quanto às exceções? Podemos lidar com eles de forma assíncrona? certamente!
Copie o código do código da seguinte forma:
CompletableFuture<String> seguro =
future.exceptionally(ex -> "Temos um problema: " + ex.getMessage());
Quando excepcionalmente() aceita uma função, o futuro original será chamado para lançar uma exceção. Teremos a oportunidade de converter esta exceção em algum valor compatível com o tipo Future para recuperar. As conversões safeFurther não gerarão mais uma exceção, mas retornarão um valor String da função que fornece a funcionalidade.
Uma abordagem mais flexível é handle() aceitar uma função que receba o resultado ou exceção correto:
Copie o código do código da seguinte forma:
CompletableFuture<Integer> seguro = future.handle((ok, ex) -> {
if (ok! = nulo) {
retornar Integer.parseInt(ok);
} outro {
log.warn("Problema", ex);
retornar -1;
}
});
handle() é sempre chamado e os resultados e exceções não são nulos. Esta é uma estratégia completa.
6. Combine dois CompletableFutures
CompletableFuture como um dos processos assíncronos é ótimo, mas realmente mostra o quão poderoso ele é quando vários desses futuros são combinados de várias maneiras.
7. Combine (vincule) esses dois futuros (thenCompose())
Às vezes você deseja executar algum valor futuro (quando estiver pronto), mas esta função também retorna um futuro. CompletableFuture é flexível o suficiente para entender que o resultado de nossa função agora deve ser usado como um futuro de nível superior, em comparação com CompletableFuture<CompletableFuture>. O método thenCompose() é equivalente ao flatMap do Scala:
Copie o código do código da seguinte forma:
<U> CompletableFuture<U> thenCompose(Function<? super T,CompletableFuture<U>> fn);
...Variações assíncronas também estão disponíveis. No exemplo a seguir, observe cuidadosamente os tipos e diferenças entre thenApply()(map) e thenCompose()(flatMap).
Copie o código do código da seguinte forma:
CompletableFuture<Documento> docFuture = //...
CompletableFuture<CompletableFuture<Duplo>> f =
docFuture.thenApply(this::calculateRelevance);
CompletableFuture<Double> relevânciaFuture =
docFuture.thenCompose(this::calculateRelevance);
//...
private CompletableFuture<Double> calculaRelevance(Document doc) //...
thenCompose() é um método importante que permite construir pipelines robustos e assíncronos sem bloquear e esperar etapas intermediárias.
8. Valores de conversão de dois futuros (thenCombine())
Quando thenCompose() é usado para encadear um futuro que depende de outro thenCombine, quando ambos são concluídos ele combina os dois futuros independentes:
Copie o código do código da seguinte forma:
<U,V> CompletableFuture<V> thenCombine(CompletableFuture<? estende U> outro, BiFunction<? super T,? super U,? estende V> fn)
...Variáveis assíncronas também estão disponíveis, supondo que você tenha dois CompletableFutures, um carregando o Cliente e outro carregando o Shop recente. Eles são completamente independentes um do outro, mas quando estiverem concluídos você deseja usar seus valores para calcular a Rota. Aqui está um exemplo privado:
Copie o código do código da seguinte forma:
CompletableFuture<Cliente> clienteFuture = loadCustomerDetails(123);
CompletableFuture<Loja> shopFuture = mais próximaLoja();
CompletableFuture<Rota> rotaFuture =
customerFuture.thenCombine(shopFuture, (custo, loja) -> findRoute(custo, loja));
//...
rota privada findRoute(Cliente cliente, Loja loja) //...
Observe que no Java 8 você pode simplesmente substituir a referência a este método::findRoute por (cust, shop) -> findRoute(cust, shop):
Copie o código do código da seguinte forma:
clienteFuture.thenCombine(shopFuture, this::findRoute);
Como você sabe, temos customerFuture e shopFuture. Em seguida, o routeFuture os envolve e "espera" que sejam concluídos. Quando estiverem prontos, ele executará a função que fornecemos para combinar todos os resultados (findRoute()). Este routeFuture será concluído quando os dois futuros básicos forem concluídos e findRoute() também for concluído.
9. Aguarde a conclusão de todos os CompletableFutures
Se em vez de gerar um novo CompletableFuture conectando esses dois resultados, quisermos apenas ser notificados quando ele for concluído, podemos usar a série de métodos thenAcceptBoth()/runAfterBoth(), (...variáveis assíncronas também estão disponíveis). Eles funcionam de forma semelhante a thenAccept() e thenRun(), mas esperam por dois futuros em vez de um:
Copie o código do código da seguinte forma:
<U> CompletableFuture<Void> thenAcceptBoth(CompletableFuture<? extends U> other, BiConsumer<? super T,? super U> bloco)
CompletableFuture<Void> runAfterBoth(CompletableFuture<?> other, ação executável)
Imagine o exemplo acima, em vez de gerar um novo CompletableFuture, você deseja apenas enviar alguns eventos ou atualizar a GUI imediatamente. Isso pode ser facilmente alcançado: thenAcceptBoth():
Copie o código do código da seguinte forma:
clienteFuture.thenAcceptBoth(shopFuture, (custo, loja) -> {
rota final rota = findRoute(cust,shop);
//atualiza GUI com rota
});
Espero estar errado, mas talvez algumas pessoas se perguntem: por que não posso simplesmente bloquear esses dois futuros? Como:
Copie o código do código da seguinte forma:
Future<Cliente> clienteFuture = loadCustomerDetails(123);
Future<Loja> lojaFuture = loja mais próxima();
findRoute(customerFuture.get(), shopFuture.get());
Bem, é claro que você pode fazer isso. Mas o ponto mais crítico é que CompletableFuture permite assincronia. É um modelo de programação orientado a eventos, em vez de bloquear e aguardar ansiosamente pelos resultados. Portanto, funcionalmente, as duas partes do código acima são equivalentes, mas a última não precisa ocupar um thread para ser executada.
10. Aguarde o primeiro CompletableFuture para concluir a tarefa
Outra coisa interessante é que a CompletableFutureAPI pode esperar até que o primeiro (em oposição a todos) o futuro seja concluído. Isso é muito conveniente quando você tem os resultados de duas tarefas do mesmo tipo. Você só se preocupa com o tempo de resposta e nenhuma tarefa tem prioridade. Métodos API (…variáveis assíncronas também estão disponíveis):
Copie o código do código da seguinte forma:
CompletableFuture<Void> acceptEither(CompletableFuture<? extends T> other, Consumer<? super T> block)
CompletableFuture<Void> runAfterEither(CompletableFuture<?> other, ação executável)
Por exemplo, você tem dois sistemas que podem ser integrados. Um tem um tempo médio de resposta menor, mas um desvio padrão alto, o outro é geralmente mais lento, mas mais previsível. Para obter o melhor dos dois mundos (desempenho e previsibilidade), você pode chamar os dois sistemas ao mesmo tempo e aguardar o que terminar primeiro. Normalmente este será o primeiro sistema, mas quando o progresso se torna lento, o segundo sistema pode ser concluído num tempo aceitável:
Copie o código do código da seguinte forma:
CompletableFuture<String> rápido = fetchFast();
CompletableFuture<String> previsível = fetchPredictably();
fast.acceptEither(previsível, s -> {
System.out.println("Resultado: " + s);
});
s representa a String obtida de fetchFast() ou fetchPredictably(). Não precisamos saber ou nos importar.
11. Converta completamente o primeiro sistema
applyToEither() é considerado o antecessor de acceptEither(). Quando dois futuros estão prestes a ser concluídos, o último simplesmente chama algum trecho de código e applyToEither() retornará um novo futuro. Quando estes dois futuros iniciais se completarem, o novo futuro também se completará. A API é um pouco semelhante (...variáveis assíncronas também estão disponíveis):
Copie o código da seguinte forma:<U> CompletableFuture<U> applyToEither(CompletableFuture<? extends T> other, Function<? super T,U> fn)
Esta função fn adicional pode ser concluída quando o primeiro futuro for chamado. Não tenho certeza de qual é o propósito deste método especializado, afinal pode-se simplesmente usar: fast.applyToEither(predictable).thenApply(fn). Como estamos presos a esta API, mas não precisamos realmente da funcionalidade extra para o aplicativo, simplesmente usarei o espaço reservado Function.identity():
Copie o código do código da seguinte forma:
CompletableFuture<String> rápido = fetchFast();
CompletableFuture<String> previsível = fetchPredictably();
CompletableFuture<String> primeiroDone =
fast.applyToEither(previsível, Function.<String>identidade());
O primeiro futuro concluído pode ser executado. Observe que, da perspectiva do cliente, ambos os futuros estão, na verdade, ocultos atrás do firstDone. O cliente apenas espera a conclusão do futuro e usa applyToEither() para notificar o cliente quando as duas primeiras tarefas forem concluídas.
12. CompletableFuture com múltiplas combinações
Agora sabemos como esperar que dois futuros sejam concluídos (usando thenCombine()) e o primeiro a ser concluído (applyToEither()). Mas pode ser dimensionado para qualquer número de futuros? Na verdade, use métodos auxiliares estáticos:
Copie o código do código da seguinte forma:
estático CompletableFuture<Void< allOf(CompletableFuture<?<... cfs)
estático CompletableFuture<Object<anyOf(CompletableFuture<?<... cfs)
allOf() usa uma matriz de futuros e retorna um futuro (aguardando todos os obstáculos) quando todos os futuros potenciais forem concluídos. Por outro lado, anyOf() aguardará os futuros potenciais mais rápidos. Por favor, observe o tipo geral de futuros retornados. Não é isso que você espera? Vamos nos concentrar nesta questão no próximo artigo.
Resumir
Exploramos toda a API CompletableFuture. Estou convencido de que isso será invencível, então no próximo artigo veremos a implementação de outro rastreador da web simples usando métodos CompletableFuture e expressões lambda Java 8. Também veremos CompletableFuture.