Java 8 ya está aquí, es hora de aprender algo nuevo. Java 7 y Java 6 son sólo versiones ligeramente modificadas, pero Java 8 tendrá mejoras importantes. ¿Quizás Java 8 es demasiado grande? Hoy les daré una explicación detallada de la nueva abstracción CompletableFuture en JDK 8. Como todos sabemos, Java 8 se lanzará en menos de un año, por lo que este artículo se basa en JDK 8 build 88 con soporte lambda. CompletableFuture extiende Future proporciona métodos, operadores unarios y promueve la asincronicidad y un modelo de programación basado en eventos que no se limita a versiones anteriores de Java. Si abre el JavaDoc de CompletableFuture, se sorprenderá. Hay alrededor de cincuenta métodos (!), y algunos de ellos son muy interesantes y difíciles de entender, por ejemplo:
Copie el código de la siguiente manera: public <U,V> CompletableFuture<V> luegoCombineAsync(
CompletableFuture<? extiende U> otro,
BiFunción<?super T,?super U,?extiende V>fn,
ejecutor ejecutor)
No te preocupes, sigue leyendo. CompletableFuture recopila todas las características de ListenableFuture en Guava y SettableFuture. Además, las expresiones lambda integradas lo acercan a los futuros de Scala/Akka. Esto puede parecer demasiado bueno para ser verdad, pero sigue leyendo. CompletableFuture tiene dos aspectos principales que son superiores a la devolución de llamada/conversión asincrónica de Future en ol, que permite establecer el valor de CompletableFuture desde cualquier hilo en cualquier momento.
1. Extraer y modificar el valor del paquete.
A menudo, los futuros representan código que se ejecuta en otros subprocesos, pero no siempre es así. A veces desea crear un Futuro para indicar que sabe lo que sucederá, como la llegada de un mensaje JMS. Entonces tiene un futuro pero no hay trabajo asincrónico potencial en el futuro. Solo desea terminar (resolver) cuando llegue un mensaje JMS futuro, impulsado por un evento. En este caso, simplemente puede crear un CompletableFuture para devolverlo a su cliente, y simplemente complete() desbloqueará todos los clientes que esperan el Futuro siempre que crea que su resultado está disponible.
Primero, simplemente puedes crear un nuevo CompletableFuture y entregárselo a tu cliente:
Copie el código de la siguiente manera: public CompletableFuture<String> Ask() {
final CompletableFuture<String> futuro = new CompletableFuture<>();
//...
retorno futuro;
}
Tenga en cuenta que este futuro no tiene conexión con Callable, no hay un grupo de subprocesos y no funciona de forma asincrónica. Si el código del cliente ahora llama a Ask().get(), se bloqueará para siempre. Si los registros completan la devolución de llamada, nunca tendrán efecto. Entonces, ¿cuál es la clave? Ahora puedes decir:
Copie el código de la siguiente manera: futuro.completo("42")
... En este momento, todos los clientes Future.get() obtendrán el resultado de la cadena y entrará en vigor inmediatamente después de completar la devolución de llamada. Esto es muy conveniente cuando desea representar la tarea de un Futuro y no es necesario calcular la tarea de algún hilo de ejecución. CompletableFuture.complete() solo se puede llamar una vez; las llamadas posteriores se ignorarán. Pero también hay una puerta trasera llamada CompletableFuture.obtrudeValue(...) que sobrescribe el valor anterior de un nuevo Futuro, así que utilícela con precaución.
A veces desea ver qué sucede cuando falla una señal, ya que sabe que un objeto Future puede manejar el resultado o excepción que contiene. Si desea pasar algunas excepciones más, puede usar CompletableFuture.completeExceptionally(ex) (o usar un método más poderoso como obtrudeException(ex) para anular la excepción anterior). completeExceptionally() también desbloquea todos los clientes en espera, pero esta vez genera una excepción de get(). Hablando de get(), también existe el método CompletableFuture.join() con cambios sutiles en el manejo de errores. Pero en general son todos iguales. Finalmente está el método CompletableFuture.getNow(valueIfAbsent) que no bloquea pero devolverá el valor predeterminado si el Future aún no se ha completado, lo que lo hace muy útil al construir sistemas robustos donde no queremos esperar demasiado.
El método estático final es utilizar completeFuture(value) para devolver el objeto Future completo, lo que puede resultar muy útil al probar o escribir algunas capas de adaptador.
2. Cree y obtenga CompletableFuture
Bien, ¿crear un CompletableFuture manualmente es nuestra única opción? incierto. Al igual que los Futures normales, podemos asociar tareas existentes y CompletableFuture utiliza métodos de fábrica:
Copie el código de código de la siguiente manera:
estático <U> CompletableFuture<U> suministroAsync(Proveedor<U> proveedor);
estático <U> CompletableFuture<U> suministroAsync(Proveedor<U> proveedor, Ejecutor ejecutor);
static CompletableFuture<Void> runAsync(Runnable ejecutable);
static CompletableFuture<Void> runAsync(Runnable ejecutable, Ejecutor ejecutor);
El método sin parámetros Executor termina con...Async y utilizará ForkJoinPool.commonPool() (grupo común global introducido en JDK8), que se aplica a la mayoría de los métodos de la clase CompletableFuture. runAsync() es fácil de entender; tenga en cuenta que requiere un Runnable, por lo que devuelve CompletableFuture<Void> ya que Runnable no devuelve ningún valor. Si necesita manejar operaciones asincrónicas y devolver resultados, use Proveedor<U>:
Copie el código de código de la siguiente manera:
final CompletableFuture<String> futuro = CompletableFuture.supplyAsync(nuevo Proveedor<String>() {
@Anular
cadena pública obtener() {
//...larga duración...
devolver "42";
}
}, ejecutor);
¡Pero no olvides que hay expresiones lambdas en Java 8!
Copie el código de código de la siguiente manera:
finalCompletableFuture<String> futuro = CompletableFuture.supplyAsync(() -> {
//...larga duración...
devolver "42";
}, ejecutor);
o:
Copie el código de código de la siguiente manera:
final CompletableFuture<String> futuro =
CompletableFuture.supplyAsync(() -> longRunningTask(params), ejecutor);
Aunque este artículo no trata sobre lambdas, utilizo expresiones lambda con bastante frecuencia.
3. Conversión y acción en CompletableFuture (luego Aplicar)
Dije CompletableFuture es mejor que Future pero ¿no sabes por qué? En pocas palabras, porque CompletableFuture es un átomo y un factor. ¿No es útil lo que dije? Tanto Scala como JavaScript le permiten registrar una devolución de llamada asincrónica cuando se completa un futuro, y no tenemos que esperar y bloquearlo hasta que esté listo. Simplemente podemos decir: cuando ejecutas esta función, aparece el resultado. Además, podemos apilar estas funciones, combinar múltiples futuros, etc. Por ejemplo, si convertimos de String a Integer, podemos convertir de CompletableFuture a CompletableFuture<Integer sin asociación. Esto se hace a través de thenApply():
Copie el código de código de la siguiente manera:
<U> CompletableFuture<U> luegoAplicar(Función<? super T,? extiende U> fn);
<U> CompletableFuture<U> luegoApplyAsync(Función<? super T,? extiende U> fn);
<U> CompletableFuture<U> luegoApplyAsync(Función<? super T,? extiende U> fn, Ejecutor ejecutor);<p></p>
<p>Como se mencionó... la versión Async proporciona la mayoría de las operaciones en CompletableFuture, por lo que las omitiré en secciones posteriores. Recuerde, el primer método llamará al método en el mismo subproceso donde se completa el futuro, mientras que los dos restantes lo llamarán de forma asincrónica en diferentes grupos de subprocesos.
Echemos un vistazo al flujo de trabajo de thenApply():</p>
<p><pre>
CompletableFuture<String> f1 = //...
CompletableFuture<Integer> f2 = f1.thenApply(Integer::parseInt);
CompletableFuture<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</p>
O en un comunicado:
Copie el código de código de la siguiente manera:
CompletableFuture<Doble> f3 =
f1.thenApply(Integer::parseInt).thenApply(r -> r * r * Math.PI);
Aquí verá la conversión de una secuencia, de Cadena a Entero y a Doble. Pero lo más importante es que estas transformaciones no se ejecutan inmediatamente ni se detienen. Estas transformaciones no se ejecutan inmediatamente ni se detienen. Simplemente recuerdan el programa que ejecutaron cuando se completó el f1 original. Si ciertas transformaciones requieren mucho tiempo, puede proporcionar su propio Ejecutor para ejecutarlas de forma asincrónica. Tenga en cuenta que esta operación es equivalente a un mapa unario en Scala.
4. Ejecute el código completo (luego Aceptar/luego Ejecutar)
Copie el código de código de la siguiente manera:
CompletableFuture<Void> luegoAceptar(Consumidor<? super T> bloque);
CompletableFuture<Void> luegoRun(Acción ejecutable);
Hay dos métodos típicos de etapa "final" en tuberías futuras. Están preparados cuando usas el valor del futuro. Cuando thenAccept() proporciona el valor final, thenRun ejecuta Runnable, que ni siquiera tiene una forma de calcular el valor. Por ejemplo:
Copie el código de código de la siguiente manera:
futuro.thenAcceptAsync(dbl -> log.debug("Resultado: {}", dbl), ejecutor);
log.debug("Continuando");
... Las variables asíncronas también están disponibles de dos maneras, ejecutores implícitos y explícitos, y no enfatizaré demasiado este método.
Los métodos thenAccept()/thenRun() no bloquean (incluso si no hay un ejecutor explícito). Son como un detector/controlador de eventos, que se ejecutará durante un período de tiempo cuando lo conecte a un futuro. El mensaje "Continuando" aparecerá inmediatamente, aunque el futuro ni siquiera está completo.
5. Manejo de errores de un único CompletableFuture
Hasta ahora sólo hemos discutido los resultados de los cálculos. ¿Qué pasa con las excepciones? ¿Podemos manejarlos de forma asincrónica? ¡ciertamente!
Copie el código de código de la siguiente manera:
CompletableFuture<String> seguro =
futuro.excepcionalmente(ex -> "Tenemos un problema: " + ex.getMessage());
Cuando excepcionalmente () acepta una función, se llamará al futuro original para generar una excepción. Tendremos la oportunidad de convertir esta excepción en algún valor compatible con el tipo Future a recuperar. Las conversiones de safeFurther ya no generarán una excepción, sino que devolverán un valor de cadena de la función que proporciona la funcionalidad.
Un enfoque más flexible es que handle() acepte una función que reciba el resultado o excepción correcto:
Copie el código de código de la siguiente manera:
CompletableFuture<Integer> seguro = futuro.handle((ok, ex) -> {
si (¡vale! = nulo) {
devolver Integer.parseInt(ok);
} demás {
log.warn("Problema", por ejemplo);
devolver -1;
}
});
Siempre se llama a handle () y los resultados y las excepciones no son nulos. Esta es una estrategia integral y única.
6. Combina dos CompletableFutures juntos
CompletableFuture como uno de los procesos asincrónicos es genial, pero realmente muestra lo poderoso que es cuando se combinan múltiples futuros de varias maneras.
7. Combine (vincule) estos dos futuros (luegoCompose())
A veces desea ejecutar algún valor futuro (cuando esté listo), pero esta función también devuelve un futuro. CompletableFuture es lo suficientemente flexible como para comprender que el resultado de nuestra función ahora debería usarse como un futuro de nivel superior, en comparación con CompletableFuture<CompletableFuture>. El método thenCompose() es equivalente al flatMap de Scala:
Copie el código de código de la siguiente manera:
<U> CompletableFuture<U> luegoCompose(Función<? super T,CompletableFuture<U>> fn);
...Las variaciones asíncronas también están disponibles en el siguiente ejemplo, observe atentamente los tipos y diferencias entre thenApply()(map) y thenCompose()(flatMap). Al aplicar el método calcularRelevance(), se devuelve un CompletableFuture:
Copie el código de código de la siguiente manera:
CompletableFuture<Documento> docFuture = //...
CompletableFuture<CompletableFuture<Doble>> f =
docFuture.thenApply(this::calculateRelevance);
CompletableFuture<Double> relevanciaFuture =
docFuture.thenCompose(this::calculateRelevance);
//...
privado CompletableFuture<Double> calcularRelevancia(Documento doc) //...
thenCompose() es un método importante que permite construir canalizaciones sólidas y asincrónicas sin bloquear ni esperar pasos intermedios.
8. Valores de conversión de dos futuros (luegoCombine())
Cuando se usa thenCompose() para encadenar un futuro que depende de otro thenCombine, cuando ambos se completan combina los dos futuros independientes:
Copie el código de código de la siguiente manera:
<U,V> CompletableFuture<V> luegoCombinar(CompletableFuture<? extiende U> otro, BiFunction<? super T,? super U,? extiende V> fn)
...Las variables asíncronas también están disponibles, asumiendo que tiene dos CompletableFutures, uno cargando el Cliente y el otro cargando la Tienda reciente. Son completamente independientes entre sí, pero cuando estén completos querrás utilizar sus valores para calcular la Ruta. Aquí hay un ejemplo privable:
Copie el código de código de la siguiente manera:
CompletableFuture<Cliente> clienteFuture = loadCustomerDetails(123);
CompletableFuture<Tienda> tiendaFuture = tienda más cercana();
CompletableFuture<Ruta> rutaFuturo =
customerFuture.thenCombine(shopFuture, (cliente, tienda) -> findRoute(cliente, tienda));
//...
Ruta privada findRoute(Cliente cliente, Tienda tienda) //...
Tenga en cuenta que en Java 8 puede simplemente reemplazar la referencia a este método::findRoute con (cust, shop) -> findRoute(cust, shop):
Copie el código de código de la siguiente manera:
customerFuture.thenCombine(shopFuture, this::findRoute);
Como sabes, tenemos customerFuture y shopFuture. Luego, routeFuture los envuelve y "espera" a que se completen. Cuando estén listos, ejecutará la función que proporcionamos para combinar todos los resultados (findRoute()). Este routeFuture se completará cuando se completen los dos futuros básicos y también se complete findRoute().
9. Espere a que se completen todos los CompletableFutures.
Si en lugar de generar un nuevo CompletableFuture conectando estos dos resultados, solo queremos que se nos notifique cuando se complete, podemos usar la serie de métodos thenAcceptBoth()/runAfterBoth() (...las variables asíncronas también están disponibles). Funcionan de manera similar a thenAccept() y thenRun(), pero esperan dos futuros en lugar de uno:
Copie el código de código de la siguiente manera:
<U> CompletableFuture<Void> luegoAceptarBoth(CompletableFuture<? extiende U> otro, BiConsumer<? super T,? super U> bloque)
CompletableFuture<Void> runAfterBoth(CompletableFuture<?> otro, acción ejecutable)
Imagine el ejemplo anterior, en lugar de generar un nuevo CompletableFuture, solo desea enviar algunos eventos o actualizar la GUI inmediatamente. Esto se puede lograr fácilmente: luegoAcceptBoth():
Copie el código de código de la siguiente manera:
customerFuture.thenAcceptBoth(shopFuture, (cliente, tienda) -> {
ruta final ruta = findRoute(cust, shop);
//actualizar GUI con ruta
});
Espero estar equivocado, pero tal vez algunas personas se hagan una pregunta: ¿por qué no puedo simplemente bloquear estos dos futuros? Como:
Copie el código de código de la siguiente manera:
Futuro<Cliente> clienteFuturo = loadCustomerDetails(123);
Futuro<Tienda> tiendaFuturo = Tienda más cercana();
findRoute(clienteFuture.get(), tiendaFuture.get());
Bueno, por supuesto que puedes hacer eso. Pero el punto más crítico es que CompletableFuture permite la asincronía. Es un modelo de programación basado en eventos en lugar de bloquear y esperar ansiosamente los resultados. Entonces, funcionalmente, las dos partes del código anteriores son equivalentes, pero la última no necesita ocupar un hilo para ejecutarse.
10. Espere a que el primer CompletableFuture complete la tarea.
Otra cosa interesante es que CompletableFutureAPI puede esperar a que se complete el primer futuro (a diferencia de todos). Esto es muy conveniente cuando tienes los resultados de dos tareas del mismo tipo. Solo te importa el tiempo de respuesta y ninguna tarea tiene prioridad. Métodos API (…las variables asíncronas también están disponibles):
Copie el código de código de la siguiente manera:
CompletableFuture<Void> aceptarEither(CompletableFuture<? extiende T> otro, Consumidor<? super T> bloque)
CompletableFuture<Void> runAfterEither(CompletableFuture<?> otro, acción ejecutable)
Como ejemplo, tiene dos sistemas que se pueden integrar. Uno tiene un tiempo de respuesta promedio menor pero una desviación estándar alta; el otro es generalmente más lento pero más predecible. Para obtener lo mejor de ambos mundos (rendimiento y previsibilidad), puede llamar a ambos sistemas al mismo tiempo y esperar a que finalice primero. Generalmente este será el primer sistema, pero cuando el progreso se vuelve lento, el segundo sistema puede completarse en un tiempo aceptable:
Copie el código de código de la siguiente manera:
CompletableFuture<String> rápido = fetchFast();
CompletableFuture<String> predecible = fetchPredictably();
fast.acceptEither(predecible, s -> {
System.out.println("Resultado: " + s);
});
s representa la cadena obtenida de fetchFast() o fetchPredictably(). No tenemos por qué saberlo ni preocuparnos.
11. Convierta completamente el primer sistema.
applyToEither() se considera el predecesor de AcceptEither(). Cuando dos futuros están a punto de completarse, este último simplemente llama a algún fragmento de código y applyToEither() devolverá un nuevo futuro. Cuando estos dos futuros iniciales se completen, el nuevo futuro también se completará. La API es algo similar (...las variables asíncronas también están disponibles):
Copie el código de la siguiente manera:<U> CompletableFuture<U> applyToEither(CompletableFuture<? extends T> other, Function<? super T,U> fn)
Esta función fn adicional se puede completar cuando se llama al primer futuro. No estoy seguro de cuál es el propósito de este método especializado; después de todo, uno podría simplemente usar: fast.applyToEither(predictable).thenApply(fn). Como estamos atrapados con esta API, pero realmente no necesitamos la funcionalidad adicional para la aplicación, simplemente usaré el marcador de posición Function.identity():
Copie el código de código de la siguiente manera:
CompletableFuture<String> rápido = fetchFast();
CompletableFuture<String> predecible = fetchPredictably();
CompletableFuture<String> primeroDone =
fast.applyToEither(predecible, Function.<String>identity());
Se puede ejecutar el primer futuro completado. Tenga en cuenta que desde la perspectiva del cliente, ambos futuros en realidad están ocultos detrás de firstDone. El cliente simplemente espera a que se complete el futuro y usa applyToEither() para notificarle cuando se completan las dos primeras tareas.
12. CompletableFuture con múltiples combinaciones
Ahora sabemos cómo esperar a que se completen dos futuros (usando thenCombine()) y que se complete el primero (applyToEither()). Pero, ¿puede ampliarse a cualquier número de futuros? De hecho, utilice métodos auxiliares estáticos:
Copie el código de código de la siguiente manera:
static CompletableFuture<Void< allOf(CompletableFuture<?<... cfs)
static CompletableFuture<Object< anyOf(CompletableFuture<?<... cfs)
allOf() utiliza una serie de futuros y devuelve un futuro (esperando todos los obstáculos) cuando todos los futuros potenciales se han completado. Por otro lado, anyOf() esperará por los futuros potenciales más rápidos. Mire el tipo general de futuros devueltos. ¿No es esto lo que espera? Nos centraremos en este tema en el próximo artículo.
Resumir
Exploramos toda la API CompletableFuture. Estoy convencido de que esto será invencible, por lo que en el próximo artículo veremos la implementación de otro rastreador web simple que utiliza métodos CompletableFuture y expresiones lambda de Java 8. También veremos CompletableFuture.