allOf completes when every supplied future finishes; anyOf completes when the first one finishes. Both return useless value types (CompletableFuture<Void> and CompletableFuture<Object>), so you collect the real results by joining the originals afterward.
allOf — wait for all (fan-in)
allOf(cf1, cf2, ...) returns a CompletableFuture<Void> that completes once all inputs are done. It carries no result — its job is to be a barrier. To get the values you go back to the source futures (now guaranteed complete) and join them:
List<CompletableFuture<Product>> futures = ids.stream()
.map(id -> CompletableFuture.supplyAsync(() -> load(id), pool))
.toList();
CompletableFuture<List<Product>> all =
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join) // safe now: all are complete
.toList());
List<Product> products = all.join();
join is like get but throws the unchecked CompletionException instead of checked exceptions — convenient inside lambdas.
anyOf — take the first (race)
anyOf(cf1, cf2, ...) returns CompletableFuture<Object> that completes with the result of whichever future finishes first. The Object type is because the inputs can be heterogeneous; cast or constrain at the call site.
CompletableFuture<Object> fastest = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> queryReplicaA()),
CompletableFuture.supplyAsync(() -> queryReplicaB()));
String winner = (String) fastest.join();
Typical uses: hedged requests (query two replicas, take the faster), or timeouts (race the real work against a delayed failure).
allOf: A ──┐
B ──┼──► (Void) completes when ALL done ──► join each for results
C ──┘
anyOf: A ──┐
B ──┼──► (Object) completes when FIRST settles
C ──┘