The 30 second answer
A Future only lets you check if the work is done, cancel it, or block on get to wait for the result. CompletableFuture, added in Java 8, lets me chain the next steps without blocking: transform the result, call another async step, combine two results, or wait for many. It also handles errors in the chain. For blocking I/O, I pass my own executor instead of using the common pool.
Future<T> (Java 5) offers isDone(), cancel() and a blocking get(). You can't attach a callback or compose two futures.
CompletableFuture<T> (Java 8) implements both Future and CompletionStage:
thenApplytransforms the result;thenComposechains another async call (likeflatMap).thenCombinemerges two independent results;allOf/anyOfwait for several.exceptionally,handleandwhenCompletedeal with failures.complete(value)lets you finish it by hand.
CompletableFuture<List<Order>> orders =
CompletableFuture.supplyAsync(() -> loadUser(id), ioPool)
.thenCompose(u -> CompletableFuture.supplyAsync(() -> loadOrders(u), ioPool))
.orTimeout(2, TimeUnit.SECONDS) // Java 9
.exceptionally(ex -> List.of());Async methods without an executor argument use ForkJoinPool.commonPool(). [Opinion] For blocking I/O pass a dedicated executor so you don't starve the common pool, which parallel streams also share. On Java 21, a virtual-thread-per-task executor is a good fit.
Sources: Future Javadoc, CompletableFuture Javadoc