CompletableFuture 的 20 個例子

王爵nice發表於2019-03-02

這篇部落格回顧JAVA8的CompletionStageAPI以及其在JAVA庫中的標準實現CompletableFuture。將會通過幾個例子來展示API的各種行為。

因為CompletableFutureCompletionInterface介面的實現,所以我們首先要了解該介面的契約。它代表某個同步或非同步計算的一個階段。你可以把它理解為是一個為了產生有價值最終結果的計算的流水線上的一個單元。這意味著多個ComletionStage指令可以連結起來從而一個階段的完成可以觸發下一個階段的執行。

除了實現了CompletionStage介面,Completion還繼承了Future,這個介面用於實現一個未開始的非同步事件。因為能夠顯式的完成Future,所以取名為CompletableFuture

1.新建一個完成的CompletableFuture

這個簡單的示例中建立了一個已經完成的預先設定好結果的CompletableFuture。通常作為計算的起點階段。

static void completedFutureExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message");
    assertTrue(cf.isDone());
    assertEquals("message", cf.getNow(null));
}
複製程式碼

getNow方法會返回完成後的結果(這裡就是message),如果還未完成,則返回傳入的預設值null

2.執行一個簡單的非同步stage

下面的例子解釋瞭如何建立一個非同步執行Runnable的stage。

static void runAsyncExample() {
    CompletableFuture cf = CompletableFuture.runAsync(() -> {
        assertTrue(Thread.currentThread().isDaemon());
        randomSleep();
    });
    assertFalse(cf.isDone());
    sleepEnough();
    assertTrue(cf.isDone());
}
複製程式碼

這個例子想要說明兩個事情:

  1. CompletableFuture中以Async為結尾的方法將會非同步執行
  2. 預設情況下(即指沒有傳入Executor的情況下),非同步執行會使用ForkJoinPool實現,該執行緒池使用一個後臺執行緒來執行Runnable任務。注意這只是特定於CompletableFuture實現,其它的CompletableStage實現可以重寫該預設行為。

3.將方法作用於前一個Stage

下面的例子引用了第一個例子中已經完成的CompletableFuture,它將引用生成的字串結果並將該字串大寫。

static void thenApplyExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message").thenApply(s -> {
        assertFalse(Thread.currentThread().isDaemon());
        return s.toUpperCase();
    });
    assertEquals("MESSAGE", cf.getNow(null));
}
複製程式碼

這裡的關鍵詞是thenApply

  1. then是指在當前階段正常執行完成後(正常執行是指沒有丟擲異常)進行的操作。在本例中,當前階段已經完成並得到值message
  2. Apply是指將一個Function作用於之前階段得出的結果

Function是阻塞的,這意味著只有當大寫操作執行完成之後才會執行getNow()方法。

4.非同步的的將方法作用於前一個Stage

通過在方法後面新增Async字尾,該CompletableFuture鏈將會非同步執行(使用ForkJoinPool.commonPool())

static void thenApplyAsyncExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message").thenApplyAsync(s -> {
        assertTrue(Thread.currentThread().isDaemon());
        randomSleep();
        return s.toUpperCase();
    });
    assertNull(cf.getNow(null));
    assertEquals("MESSAGE", cf.join());
}
複製程式碼

使用一個自定義的Executor來非同步執行該方法

非同步方法的一個好處是可以提供一個Executor來執行CompletableStage。這個例子展示瞭如何使用一個固定大小的執行緒池來實現大寫操作。

static ExecutorService executor = Executors.newFixedThreadPool(3, new ThreadFactory() {
    int count = 1;
    @Override
    public Thread newThread(Runnable runnable) {
        return new Thread(runnable, "custom-executor-" + count++);
    }
});
static void thenApplyAsyncWithExecutorExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message").thenApplyAsync(s -> {
        assertTrue(Thread.currentThread().getName().startsWith("custom-executor-"));
        assertFalse(Thread.currentThread().isDaemon());
        randomSleep();
        return s.toUpperCase();
    }, executor);
    assertNull(cf.getNow(null));
    assertEquals("MESSAGE", cf.join());
}
複製程式碼

6.消費(Consume)前一個Stage的結果

如果下一個Stage接收了當前Stage的結果但是在計算中無需返回值(比如其返回值為void),那麼它將使用方法thenAccept並傳入一個Consumer介面。

static void thenAcceptExample() {
    StringBuilder result = new StringBuilder();
    CompletableFuture.completedFuture("thenAccept message")
            .thenAccept(s -> result.append(s));
    assertTrue("Result was empty", result.length() > 0);
}
複製程式碼

Consumer將會同步執行,所以我們無需在返回的CompletableFuture上執行join操作。

7.非同步執行Comsume

同樣,使用Asyn字尾實現:

static void thenAcceptAsyncExample() {
    StringBuilder result = new StringBuilder();
    CompletableFuture cf = CompletableFuture.completedFuture("thenAcceptAsync message")
            .thenAcceptAsync(s -> result.append(s));
    cf.join();
    assertTrue("Result was empty", result.length() > 0);
}
複製程式碼

8.計算出現異常時

我們現在來模擬一個出現異常的場景。為了簡潔性,我們還是將一個字串大寫,但是我們會模擬延時進行該操作。我們會使用thenApplyAsyn(Function, Executor),第一個引數是大寫轉化方法,第二個引數是一個延時executor,它會延時一秒鐘再將操作提交給ForkJoinPool

static void completeExceptionallyExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message").thenApplyAsync(String::toUpperCase,
            CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS));
    CompletableFuture exceptionHandler = cf.handle((s, th) -> { return (th != null) ? "message upon cancel" : ""; });
    cf.completeExceptionally(new RuntimeException("completed exceptionally"));
    assertTrue("Was not completed exceptionally", cf.isCompletedExceptionally());
    try {
        cf.join();
        fail("Should have thrown an exception");
    } catch(CompletionException ex) { // just for testing
        assertEquals("completed exceptionally", ex.getCause().getMessage());
    }
    assertEquals("message upon cancel", exceptionHandler.join());
}
複製程式碼
  1. 首先,我們新建了一個已經完成並帶有返回值messageCompletableFuture物件。然後我們呼叫thenApplyAsync方法,該方法會返回一個新的CompletableFuture。這個方法用非同步的方式執行大寫操作。這裡還展示瞭如何使用delayedExecutor(timeout, timeUnit)方法來延時非同步操作。
  2. 然後我們建立了一個handler stage,exceptionHandler,這個階段會處理一切異常並返回另一個訊息message upon cancel
  3. 最後,我們顯式的完成第二個階段並丟擲異常,它會導致進行大寫操作的階段丟擲CompletionException。它還會觸發handler階段。

API補充:
<U> CompletableFuture<U> handle(BiFunction<? super T,Throwable,? extends U> fn)
返回一個新的CompletionStage,無論之前的Stage是否正常執行完畢。傳入的引數包括上一個階段的結果和丟擲異常。

9.取消計算

和計算時異常處理很相似,我們可以通過Future介面中的cancel(boolean mayInterruptIfRunning)來取消計算。

static void cancelExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message").thenApplyAsync(String::toUpperCase,
            CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS));
    CompletableFuture cf2 = cf.exceptionally(throwable -> "canceled message");
    assertTrue("Was not canceled", cf.cancel(true));
    assertTrue("Was not completed exceptionally", cf.isCompletedExceptionally());
    assertEquals("canceled message", cf2.join());
}
複製程式碼

API補充
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
返回一個新的CompletableFuture,如果出現異常,則為該方法中執行的結果,否則就是正常執行的結果。

10.將Function作用於兩個已完成Stage的結果之一

下面的例子建立了一個CompletableFuture物件並將Function作用於已完成的兩個Stage中的任意一個(沒有保證哪一個將會傳遞給Function)。這兩個階段分別如下:一個將字串大寫,另一個小寫。

static void applyToEitherExample() {
    String original = "Message";
    CompletableFuture cf1 = CompletableFuture.completedFuture(original)
            .thenApplyAsync(s -> delayedUpperCase(s));
    CompletableFuture cf2 = cf1.applyToEither(
            CompletableFuture.completedFuture(original).thenApplyAsync(s -> delayedLowerCase(s)),
            s -> s + " from applyToEither");
    assertTrue(cf2.join().endsWith(" from applyToEither"));
}
複製程式碼

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T,U> fn)
返回一個全新的CompletableFuture,包含著this或是other操作完成之後,在二者中的任意一個執行fn

11.消費兩個階段的任意一個結果

和前一個例子類似,將Function替換為Consumer

static void acceptEitherExample() {
    String original = "Message";
    StringBuilder result = new StringBuilder();
    CompletableFuture cf = CompletableFuture.completedFuture(original)
            .thenApplyAsync(s -> delayedUpperCase(s))
            .acceptEither(CompletableFuture.completedFuture(original).thenApplyAsync(s -> delayedLowerCase(s)),
                    s -> result.append(s).append("acceptEither"));
    cf.join();
    assertTrue("Result was empty", result.toString().endsWith("acceptEither"));
}
複製程式碼

12.在兩個階段都完成後執行Runnable

注意這裡的兩個Stage都是同步執行的,第一個stage將字串轉化為大寫之後,第二個stage將其轉化為小寫。

static void runAfterBothExample() {
    String original = "Message";
    StringBuilder result = new StringBuilder();
    CompletableFuture.completedFuture(original).thenApply(String::toUpperCase).runAfterBoth(
            CompletableFuture.completedFuture(original).thenApply(String::toLowerCase),
            () -> result.append("done"));
    assertTrue("Result was empty", result.length() > 0);
}
複製程式碼

13.用Biconsumer接收兩個stage的結果

BiConsumer支援同時對兩個Stage的結果進行操作。

static void thenAcceptBothExample() {
    String original = "Message";
    StringBuilder result = new StringBuilder();
    CompletableFuture.completedFuture(original).thenApply(String::toUpperCase).thenAcceptBoth(
            CompletableFuture.completedFuture(original).thenApply(String::toLowerCase),
            (s1, s2) -> result.append(s1 + s2));
    assertEquals("MESSAGEmessage", result.toString());
}
複製程式碼

14.將Bifunction同時作用於兩個階段的結果

如果CompletableFuture想要合併兩個階段的結果並且返回值,我們可以使用方法thenCombine。這裡的計算流都是同步的,所以最後的getNow()方法會獲得最終結果,即大寫操作和小寫操作的結果的拼接。

static void thenCombineExample() {
    String original = "Message";
    CompletableFuture cf = CompletableFuture.completedFuture(original).thenApply(s -> delayedUpperCase(s))
            .thenCombine(CompletableFuture.completedFuture(original).thenApply(s -> delayedLowerCase(s)),
                    (s1, s2) -> s1 + s2);
    assertEquals("MESSAGEmessage", cf.getNow(null));
}
複製程式碼

15.非同步將Bifunction同時作用於兩個階段的結果

和之前的例子類似,只是這裡用了不同的方法:即兩個階段的操作都是非同步的。那麼thenCombine也會非同步執行,及時它沒有Async字尾。

static void thenCombineAsyncExample() {
    String original = "Message";
    CompletableFuture cf = CompletableFuture.completedFuture(original)
            .thenApplyAsync(s -> delayedUpperCase(s))
            .thenCombine(CompletableFuture.completedFuture(original).thenApplyAsync(s -> delayedLowerCase(s)),
                    (s1, s2) -> s1 + s2);
    assertEquals("MESSAGEmessage", cf.join());
}
複製程式碼

16.Compose CompletableFuture

我們可以使用thenCompose來完成前兩個例子中的操作。

static void thenComposeExample() {
    String original = "Message";
    CompletableFuture cf = CompletableFuture.completedFuture(original).thenApply(s -> delayedUpperCase(s))
            .thenCompose(upper -> CompletableFuture.completedFuture(original).thenApply(s -> delayedLowerCase(s))
                    .thenApply(s -> upper + s));
    assertEquals("MESSAGEmessage", cf.join());
}
複製程式碼

17.當多個階段中有有何一個完成,即新建一個完成階段

static void anyOfExample() {
    StringBuilder result = new StringBuilder();
    List messages = Arrays.asList("a", "b", "c");
    List<CompletableFuture> futures = messages.stream()
            .map(msg -> CompletableFuture.completedFuture(msg).thenApply(s -> delayedUpperCase(s)))
            .collect(Collectors.toList());
    CompletableFuture.anyOf(futures.toArray(new CompletableFuture[futures.size()])).whenComplete((res, th) -> {
        if(th == null) {
            assertTrue(isUpperCase((String) res));
            result.append(res);
        }
    });
    assertTrue("Result was empty", result.length() > 0);
}
複製程式碼

18.當所有的階段完成,新建一個完成階段

static void allOfExample() {
    StringBuilder result = new StringBuilder();
    List messages = Arrays.asList("a", "b", "c");
    List<CompletableFuture> futures = messages.stream()
            .map(msg -> CompletableFuture.completedFuture(msg).thenApply(s -> delayedUpperCase(s)))
            .collect(Collectors.toList());
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]))
        .whenComplete((v, th) -> {
            futures.forEach(cf -> assertTrue(isUpperCase(cf.getNow(null))));
            result.append("done");
        });
    assertTrue("Result was empty", result.length() > 0);
}
複製程式碼

19.當所有階段完成以後,新建一個非同步完成階段

static void allOfAsyncExample() {
    StringBuilder result = new StringBuilder();
    List messages = Arrays.asList("a", "b", "c");
    List<CompletableFuture> futures = messages.stream()
            .map(msg -> CompletableFuture.completedFuture(msg).thenApplyAsync(s -> delayedUpperCase(s)))
            .collect(Collectors.toList());
    CompletableFuture allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]))
            .whenComplete((v, th) -> {
                futures.forEach(cf -> assertTrue(isUpperCase(cf.getNow(null))));
                result.append("done");
            });
    allOf.join();
    assertTrue("Result was empty", result.length() > 0);
}
複製程式碼

20.真實場景

下面展示了一個實踐CompletableFuture的場景:

  1. 先通過呼叫cars()方法非同步獲得Car列表。它將會返回一個CompletionStage<List<Car>>cars()方法應當使用一個遠端的REST端點來實現。
  2. 我們將該Stage和另一個Stage組合,另一個Stage會通過呼叫rating(manufactureId)來非同步獲取每輛車的評分。
  3. 當所有的Car物件都填入評分後,我們呼叫allOf()來進入最終Stage,它將在這兩個階段完成後執行
  4. 在最終Stage上使用whenComplete(),列印出車輛的評分。
cars().thenCompose(cars -> {
    List<CompletionStage> updatedCars = cars.stream()
            .map(car -> rating(car.manufacturerId).thenApply(r -> {
                car.setRating(r);
                return car;
            })).collect(Collectors.toList());
    CompletableFuture done = CompletableFuture
            .allOf(updatedCars.toArray(new CompletableFuture[updatedCars.size()]));
    return done.thenApply(v -> updatedCars.stream().map(CompletionStage::toCompletableFuture)
            .map(CompletableFuture::join).collect(Collectors.toList()));
}).whenComplete((cars, th) -> {
    if (th == null) {
        cars.forEach(System.out::println);
    } else {
        throw new RuntimeException(th);
    }
}).toCompletableFuture().join();
複製程式碼

原文連結: 20 Examples of Using Java’s CompletableFuture – DZone Java
譯文連結: 使用JAVA CompletableFuture的20例子

相關文章