Mejora de allOf y anyOf en CompletableFuture



Hola de nuevo En previsión del comienzo del curso, "Java Developer" preparó una traducción de material útil para usted.


Hay dos métodos en CompletableFuture cuyo diseño me sorprende:

  • CompletableFuture # allOf
  • CompletableFuture # anyOf


En este artículo veremos qué les pasa y cómo pueden hacerse más convenientes.

CompletableFuture # allOf


Veamos la firma del método:

 public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) { // ... } 


Aquí hay al menos dos cuestiones polémicas:

  1. El método acepta varios objetos CompletableFuture que devuelven objetos de diferentes tipos.
  2. El método devuelve CompletableFuture , que devuelve Void


Además, puede que a algunos no les guste el número variable de parámetros, así que veamos también esta parte.

CompletableFuture<Void> menudo se usa como una señal para completar una operación; sin embargo, al hacer un pequeño cambio en la API, este método se puede usar tanto como dispositivo de señalización como como portador de los resultados de todas las operaciones completadas . Intentemos hacerlo.

Asíncrono CompletableFuture # allOf


Primero, busquemos la firma correcta.

Es justo suponer que en la mayoría de los casos se requerirá procesar una lista de CompletableFuture homogénea y devolver un CompletableFuture contenga una lista de resultados:

 public static <T> CompletableFuture<List<T>> allOf( Collection<CompletableFuture<T>> futures) { // ... } 


Las entrañas del método original son probablemente más complejas de lo que espera:

 static CompletableFuture<Void> andTree( CompletableFuture<?>[] cfs, int lo, int hi) { CompletableFuture<Void> d = new CompletableFuture<Void>(); if (lo > hi) // empty d.result = NIL; else { CompletableFuture<?> a, b; int mid = (lo + hi) >>> 1; if ((a = (lo == mid ? cfs[lo] : andTree(cfs, lo, mid))) == null || (b = (lo == hi ? a : (hi == mid+1) ? cfs[hi] : andTree(cfs, mid+1, hi))) == null) throw new NullPointerException(); if (!d.biRelay(a, b)) { BiRelay<?,?> c = new BiRelay<>(d, a, b); a.bipush(b, c); c.tryFire(SYNC); } } return d; } 


Por lo tanto, en lugar de crearlo desde cero, intentaremos reutilizar lo que ya está en el método original como si estuviera destinado a ser utilizado como un dispositivo de señalización de finalización ... y luego simplemente cambie el resultado nulo a la lista futura:

 CompletableFuture<List<CompletableFuture<T>>> i = futures.stream() .collect(collectingAndThen( toList(), l -> CompletableFuture.allOf(l.toArray(new CompletableFuture[0])) .thenApply(__ -> l))); 


Hasta ahora todo bien. Nos las arreglamos para conseguir
CompletableFuture<List<CompletableFuture<T> > > CompletableFuture<Void> lugar de CompletableFuture<Void> , que ya es bueno. Pero no necesitamos una lista de futuro con resultados, necesitamos una lista de resultados.

Ahora podemos simplemente procesar la lista y eliminar el futuro no deseado de ella. Es perfectamente normal llamar a los métodos de unión CompletableFuture #, porque sabemos que nunca se bloquearán (en este punto, todos los futuros ya están completos):

 CompletableFuture<List<T>> result = intermediate .thenApply(list -> list.stream() .map(CompletableFuture::join) .collect(toList())); 


Ahora combinemos todo esto en una solución final:

 public static <T> CompletableFuture<List<T>> allOf( Collection<CompletableFuture<T>> futures) { return futures.stream() .collect(collectingAndThen( toList(), l -> CompletableFuture.allOf(l.toArray(new CompletableFuture[0])) .thenApply(__ -> l.stream() .map(CompletableFuture::join) .collect(Collectors.toList())))); } 


Asíncrono y caída CompletableFuture # allOf


Si hay excepciones, el CompletableFuture # allOf original espera a que se completen todas las operaciones restantes.

Y si deseamos informar la finalización de la operación cuando se produce una excepción, tendremos que cambiar la implementación.

Para hacer esto, cree una nueva instancia de CompletableFuture y finalícela manualmente después de que una de las operaciones genere una excepción:

 CompletableFuture<List<T>> result = new CompletableFuture<>(); futures.forEach(f -> f .handle((__, ex) -> ex == null || result.completeExceptionally(ex))); 


... pero luego tenemos que lidiar con el escenario cuando todo el futuro se completará con éxito. Esto se puede hacer fácilmente usando el método mejorado allOf (), y luego simplemente terminar el futuro manualmente:

 allOf(futures).thenAccept(result::complete); 


Ahora podemos combinar todo para formar la solución final:

 public static <T> CompletableFuture<List<T>> allOfShortcircuiting(Collection<CompletableFuture<T>> futures) { CompletableFuture<List<T>> result = new CompletableFuture<>(); for (CompletableFuture<?> f : futures) { f.handle((__, ex) -> ex == null || result.completeExceptionally(ex)); } allOf(futures).thenAccept(result::complete); return result; } 


CompletableFuture # anyOf


Comencemos con la firma del método:

 public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) { // ... } 


Podemos detectar inmediatamente los mismos problemas que con el método discutido anteriormente:

  1. El método acepta varios objetos CompletableFuture que contienen objetos de diferentes tipos.
  2. El método devuelve un CompletableFuture contiene un objeto de tipo Object .


Según tengo entendido, el CompletableFuture#allOf fue diseñado para ser utilizado como un dispositivo de señalización. Pero CompletableFuture#anyOf no sigue esta filosofía, devolviendo un CompletableFuture<Object> , que es aún más confuso.

Eche un vistazo al siguiente ejemplo donde estoy tratando de procesar CompletableFuture que contiene datos de diferentes tipos:

 CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(1); CompletableFuture<String> f2 = CompletableFuture.completedFuture("2"); Integer result = CompletableFuture.anyOf(f1, f2) .thenApply(r -> { if (r instanceof Integer) { return (Integer) r; } else if (r instanceof String) { return Integer.valueOf((String) r); } throw new IllegalStateException("unexpected object type!"); }).join(); 


Bastante incómodo, ¿no?

Afortunadamente, esto es bastante fácil de adaptar para un escenario más común (esperando uno de los muchos valores futuros que contengan el mismo tipo) cambiando la firma e introduciendo la conversión directa de tipos.

Por lo tanto, con nuestras mejoras, podemos reutilizar los métodos existentes y obtener el resultado de forma segura:

 public static <T> CompletableFuture<T> anyOf(List<CompletableFuture<T>> cfs) { return CompletableFuture.anyOf(cfs.toArray(new CompletableFuture[0])) .thenApply(o -> (T) o); } public static <T> CompletableFuture<T> anyOf(CompletableFuture<T>... cfs) { return CompletableFuture.anyOf(cfs).thenApply(o -> (T) o); } 


Código fuente


El código fuente se puede encontrar en Github .

Eso es todo. Nos vemos en el curso .

Source: https://habr.com/ru/post/481804/


All Articles