Я думаю, что одним из преимуществ является лучшее использование пулов потоков. Пример кода ExecutorService использует один и тот же пул для каждой операции. Эти операции могут быть интенсивными ввода-вывода или вычисления. Выполнение этих операций в разных пулах лучше использует системные ресурсы. (*) Очень легко запускать задачи в разных пулах с асинхронными методами CompletableFuture.
CompletableFuture.supplyAsync(()->comp1()) // start in common-pool
.thenApplyAsync(order -> io1(order),ioPool) // lets say this is IO operation
.thenApplyAsync(order -> comp2(order)) // switch back to common-pool
.thenApplyAsync(order -> io2(order),ioPool); // another io
В этом примере, когда задача comp1 завершается, задача io1будет выполняться в пуле потоков ввода-вывода, и потоки общего пула могут выполнять другие задачи в течение этого времени. В конце задачи io1 задача comp2 будет передана в общий пул.
Вы можете достичь того же, не используя CompletableFuture, но код будет более сложным. (например, передача задачи comp2 методу io1 в качестве параметра и передача его из метода io1 в общий пул в конце.)
Кроме того, при написании асинхронного кода, я думаю, что конвейер completetableFuture должен быть завершен с другим асинхронным вызовом вместоМетод get.
(*) Скажем, это код, работающий на 8-ядерном компьютере, отправка задачи 100 вычислений в этот пул из 100 потоков не будет работать лучше, чем при запуске их 8 одновременно.