【代碼質量】-藉助JDK8提供的CompletableFuture寫出優雅的代碼

前言:要寫一手優雅的代碼,背後要經歷很多的積累和沉澱,有點類似臺上一分鐘,臺下十年功.要讓代碼變得優雅有很多種方式,使用JDK8提供的新特性便是其中一種,雖然相較於設計模式對代碼質量提升有限,但仍值得去學習和使用.本篇僅介CompletableFuture.


 

早在JDK1.5中就提供了Future和Callable來獲取異步任務的結果,但因爲獲取結果阻塞的原因,並沒有真正實現異步帶來的價值.體驗非常不好,於是JDK在1.8中終於出了Future的增強版CompletableFuture,藉助CompletableFuture提供的異步能力和lambda表達式風格,可以寫出優雅的異步編程代碼. 

 


目錄

 

1.創建CompletableFuture對象

2.同步獲取計算結果

3.計算完成後的處理

4.處理多階段任務

5.組合任務

6.Tips

7.代碼演示

8.總結


1.創建CompletableFuture對象

CompletableFuture提供了4個靜態方法用於創建CompletableFuture實例:

CompletableFuture<Void> runAsync(Runnable runnable)
CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)

CompletableFuture<U> supplyAsync(Supplier<U> supplier)
CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)

其中帶Executor參數的用於指定自定義異步執行線程池,如果不指定則默認使用JDK提供的FrokJoinPool.commonPool().

如果無需關心執行後的結果,使用runAsync創建即可,否則使用supplyAsync創建CompletableFuture.

2.同步獲取計算結果

CompletableFuture提供4種用於同步等待獲取計算結果的方法,這點跟老的Future沒啥兩樣,只不過額外提供了getNow和join方法.

public T    get()
public T    get(long timeout, TimeUnit unit)
public T    getNow(T valueIfAbsent)
public T    join()

getNow用於立即獲取返回結果,如果此時被調用方法還沒返回值,則返回指定的valueIfAbsent.

join方法和get作用一樣,用於阻塞獲取執行結果,不同的是join在調用方法出現異常時僅會返回uncheckedException,而get則返回具體異常.

3.計算完成後的處理

CompletableFuture可以通過whenComplete/whenCompleteAsync來獲取計算結果並處理,不再需要阻塞獲取,而是以異步通知回調的方式.

public CompletableFuture<T>     whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T>     whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T>     whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T>     exceptionally(Function<Throwable,? extends T> fn)

 

4.處理多階段任務

如果一個任務需要多個階段才能最終完成,那麼可以採用thenApply這種方式鏈式調用完成整個任務.

public <U> CompletableFuture<U>     thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U>     thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U>     thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

也可以採用handle方式:

public <U> CompletableFuture<U>     handle(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U>     handleAsync(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U>     handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)

與thenApply不同的是,handle是在任務完成後再執行,且可以處理異常,thenApply遇到異常則會拋出.

如果任務無需返回,只需要處理結果,那麼可以使用消費者

public CompletableFuture<Void>  thenAccept(Consumer<? super T> action)
public CompletableFuture<Void>  thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void>  thenAcceptAsync(Consumer<? super T> action, Executor executor)

5.組合任務

thenAcceptBoth用於組合兩個任務,當兩個任務都計算完成時執行:

public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,
        BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
        BiConsumer<? super T, ? super U> action)

thenAcceptEither只要兩個任務中有一個先完成了即執行:

    public <U> CompletableFuture<U> applyToEither(
        CompletionStage<? extends T> other, Function<? super T, U> fn)

如果要組合的任務超過2個,可以用:

任意一個完成:

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

全部完成:

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

6.Tips

JDK8CompletableFuture API中提供了五十多個靜態方法,乍看上去很多,其實實際開發中經常被用到的連一半都不到,再加上大部分其實是方法重載,以及功能類似方法,抽象一下最後只需要掌握十個以內的方法,便可以駕馭CompletableFuture寫出優雅代碼.

創建CompletableFuture對象時,以run打頭的方法無返回值,以supply打頭的方法有返回,可以對返回結果進一步處理.

處理任務時,方法名中包含Apply的方法有返回值,是生產者,方法名中包含Accept的方法無返回值,是消費者.

以Async結尾的方法表明該方法異步執行.


7.代碼演示

上面都是純純的理論部分,並沒有結合實際代碼演示,所以下面正式結合實際代碼演示之:

幾種經常會用到場景如下所示,如果不需要處理返回結果,可以將Apply替換爲Accept作爲純消費者即可

public class Client {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //case1:任務需要多個階段才能完成,完成執行後續動作...
        CompletableFuture.supplyAsync(() -> "hello")
                .thenApplyAsync(s -> s + " ")
                .thenApplyAsync(s -> s + "world")
                .thenApplyAsync(String::toUpperCase)
                .whenCompleteAsync((result, throwable) -> System.out.println(result));

        //case2:完成任務需要調用rpc接口,有很多個rpc接口,只要其中一個調用成功即算完成任務
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "hello");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                //模擬調用延遲
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "world";
        });
        //省略其它rpc接口...
        CompletableFuture<Object> res = CompletableFuture.anyOf(future1, future2);
        System.out.println(res.get());

        //case3:完成任務需要所有的rpc接口都完成纔算完成
        //如果使用allOf是沒有返回結果的
        CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);
        System.out.println(all.get());
        //所以如果需要獲取最終返回的結果,需要手動處理異步返回的結果
        String combine = Stream.of(future1, future2)
                .map(CompletableFuture::join)
                .collect(Collectors.joining());
        System.out.println(combine);
    }
}

運行結果:

 

8.總結

CompletableFuture封裝的真的好,以至於我在寫上面這段代碼時都不想寫下去了,真的太簡單了,沒啥好演示的,只要有jdk8的lambda基礎和functional接口基礎,就可以輕鬆上手,之所以寫這篇也是爲了做個筆記,總結下然後加深印象,感謝你的閱讀,文中若有不正之處,歡迎留言斧正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章