1. 序章

このチュートリアルは、Java 8ConcurrencyAPIの改善として導入されたCompletableFutureクラスの機能とユースケースのガイドです。

2. Javaでの非同期計算

非同期計算について推論するのは困難です。 通常、計算は一連のステップと見なしますが、非同期計算の場合、コールバックとして表されるアクションは、コード全体に分散するか、相互に深くネストされる傾向があります。 いずれかの手順で発生する可能性のあるエラーを処理する必要がある場合、事態はさらに悪化します。

FutureインターフェースはJava5で追加され、非同期計算の結果として機能しましたが、これらの計算を組み合わせたり、発生する可能性のあるエラーを処理したりする方法はありませんでした。

Java8はCompletableFutureクラスを導入しました。Future インターフェースに加えて、CompleteStageインターフェースも実装しました。 このインターフェースは、他のステップと組み合わせることができる非同期計算ステップのコントラクトを定義します。

CompletableFuture は、ビルディングブロックであると同時にフレームワークであり、非同期計算ステップを構成、結合、実行し、エラーを処理するための約50の異なるメソッドを備えています

このような大規模なAPIは圧倒される可能性がありますが、これらは主にいくつかの明確で明確なユースケースに分類されます。

3. CompleteableFutureを単純なFutureとして使用する

まず、CompletableFutureクラスはFutureインターフェイスを実装しているため、 Future実装として使用できますが、追加の完了ロジックがあります。

たとえば、引数なしのコンストラクターを使用してこのクラスのインスタンスを作成し、将来の結果を表し、それをコンシューマーに渡し、completeメソッドを使用して将来のある時点で完了することができます。 コンシューマーは、 get メソッドを使用して、この結果が提供されるまで現在のスレッドをブロックできます。

以下の例では、 CompleteableFuture インスタンスを作成し、別のスレッドで計算をスピンオフして、Futureをすぐに返すメソッドがあります。

計算が完了すると、メソッドは complete メソッドに結果を提供することにより、Futureを完了します。

public Future<String> calculateAsync() throws InterruptedException {
    CompletableFuture<String> completableFuture = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });

    return completableFuture;
}

計算をスピンオフするには、 ExecutorAPIを使用します。 CompleteableFuture を作成して完了するこの方法は、生のスレッドを含む任意の同時実行メカニズムまたはAPIと一緒に使用できます。

calculateAsyncメソッドがFutureインスタンスを返すことに注意してください。

メソッドを呼び出し、 Future インスタンスを受け取り、結果をブロックする準備ができたら、そのインスタンスでgetメソッドを呼び出すだけです。

また、 get メソッドがいくつかのチェックされた例外、つまり ExecutionException (計算中に発生した例外をカプセル化する)および InterruptedException (スレッドを示す例外)をスローすることにも注意してください。メソッドの実行が中断されました):

Future<String> completableFuture = calculateAsync();

// ... 

String result = completableFuture.get();
assertEquals("Hello", result);

計算の結果がすでにわかっている場合、この計算の結果を表す引数を指定して静的completedFutureメソッドを使用できます。 したがって、Futuregetメソッドはブロックされず、代わりに次の結果がすぐに返されます。

Future<String> completableFuture = 
  CompletableFuture.completedFuture("Hello");

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

別のシナリオとして、Futureの実行をキャンセルしたい場合があります。

4. カプセル化された計算ロジックを備えたCompletableFuture

上記のコードを使用すると、同時実行のメカニズムを選択できますが、この定型文をスキップして、コードを非同期で実行したい場合はどうでしょうか。

静的メソッドrunAsyncおよびsupplyAsyncを使用すると、RunnableおよびSupplier機能タイプからそれぞれCompletableFutureインスタンスを作成できます。 。

RunnableSupplierはどちらも、新しいJava 8機能のおかげで、インスタンスをラムダ式として渡すことができる機能インターフェイスです。

Runnable インターフェースは、スレッドで使用されているものと同じ古いインターフェースであり、値を返すことはできません。

Supplier インターフェースは、引数がなく、パラメーター化されたタイプの値を返す単一のメソッドを持つ汎用の機能インターフェースです。

これにより、は、計算を実行して結果を返すラムダ式としてSupplierのインスタンスを提供できます。 それは次のように簡単です:

CompletableFuture<String> future
  = CompletableFuture.supplyAsync(() -> "Hello");

// ...

assertEquals("Hello", future.get());

5. 非同期計算の処理結果

計算結果を処理する最も一般的な方法は、それを関数にフィードすることです。 thenApplyメソッドはまさにそれを行います。 Function インスタンスを受け入れ、それを使用して結果を処理し、関数によって返された値を保持するFutureを返します。

CompletableFuture<String> completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future = completableFuture
  .thenApply(s -> s + " World");

assertEquals("Hello World", future.get());

Future チェーンの下位に値を返す必要がない場合は、Consumer機能インターフェイスのインスタンスを使用できます。 その単一のメソッドはパラメーターを受け取り、voidを返します。

このユースケースの方法は、 CompletableFuture。 The thenAccept メソッドは消費者計算結果を渡します。 次に、最後の future.get()呼び出しは、Voidタイプのインスタンスを返します。

CompletableFuture<String> completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<Void> future = completableFuture
  .thenAccept(s -> System.out.println("Computation returned: " + s));

future.get();

最後に、計算の値が必要ない場合、またはチェーンの最後に値を返したくない場合は、RunnableラムダをthenRunメソッドに渡すことができます。 次の例では、 future.get():を呼び出した後、コンソールに行を出力するだけです。

CompletableFuture<String> completableFuture 
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<Void> future = completableFuture
  .thenRun(() -> System.out.println("Computation finished."));

future.get();

6. 未来を組み合わせる

CompletableFuture APIの最も優れた点は、一連の計算ステップでCompletableFutureインスタンスを組み合わせる機能です。

この連鎖の結果は、それ自体が CompleteableFuture であり、さらなる連鎖と結合を可能にします。 このアプローチは関数型言語に広く普及しており、モナディックデザインパターンと呼ばれることがよくあります。

次の例では、 thenCompose メソッドを使用して、2つのFuturesを順番にチェーンします。

このメソッドは、CompleteableFutureインスタンスを返す関数を受け取ることに注意してください。 この関数の引数は、前の計算ステップの結果です。 これにより、次のCompleteableFutureのラムダ内でこの値を使用できます。

CompletableFuture<String> completableFuture 
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));

assertEquals("Hello World", completableFuture.get());

thenCompose メソッドは、 thenApply、とともに、モナディックパターンの基本的な構成要素を実装します。 これらは、Java8でも使用可能なStreamおよびOptionalクラスのmapおよびflatMapメソッドと密接に関連しています。

どちらのメソッドも関数を受け取り、それを計算結果に適用しますが、 thenCompose flatMap )メソッドは、同じタイプの別のオブジェクトを返す関数を受け取ります。 この機能構造により、これらのクラスのインスタンスをビルディングブロックとして構成できます。

2つの独立したFuturesを実行し、その結果を処理する場合は、FutureFunction[を受け入れるthenCombineメソッドを使用できます。 X184X]両方の結果を処理するための2つの引数:

CompletableFuture<String> completableFuture 
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCombine(CompletableFuture.supplyAsync(
      () -> " World"), (s1, s2) -> s1 + s2));

assertEquals("Hello World", completableFuture.get());

より単純なケースは、2つの Futures の結果を使用して何かを実行したいが、結果の値をFutureチェーンに渡す必要がない場合です。 thenAcceptBothメソッドが役立ちます。

CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
  .thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
    (s1, s2) -> System.out.println(s1 + s2));

7. thenApply() thenCompose()の違い

前のセクションでは、 thenApply()および thenCompose()に関する例を示しました。 どちらのAPIも、さまざまな CompleteableFuture 呼び出しをチェーンするのに役立ちますが、これら2つの関数の使用法は異なります。

7.1. thenApply()

このメソッドを使用して、前の呼び出しの結果を処理できます。ただし、覚えておくべき重要な点は、returnタイプがすべての呼び出しの組み合わせになることです。

したがって、このメソッドは、CompleteableFuture呼び出しの結果を変換する場合に役立ちます。

CompletableFuture<Integer> finalResult = compute().thenApply(s-> s + 1);

7.2. thenCompose()

thenCompose()メソッドは、 thenApply()と似ており、どちらも新しい完了ステージを返します。 ただし、 thenCompose()は前のステージを引数として使用します。 thenApply():で観察したように、ネストされたfutureではなく、フラット化して Future を返し、結果を直接返します。

CompletableFuture<Integer> computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture<Integer> finalResult = compute().thenCompose(this::computeAnother);

したがって、 CompleteableFuture メソッドをチェーンするというアイデアの場合は、 thenCompose()を使用することをお勧めします。

また、これら2つのメソッドの違いは、 map()とflatMap()の違いに類似していることに注意してください。

8. 複数のFuturesを並行して実行する

複数のFuturesを並行して実行する必要がある場合、通常、それらすべてが実行されるのを待ってから、それらを組み合わせた結果を処理します。

CompletableFuture.allOf 静的メソッドを使用すると、var-argとして提供されるすべてのFuturesの完了を待機できます。

CompletableFuture<String> future1  
  = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2  
  = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture<String> future3  
  = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<Void> combinedFuture 
  = CompletableFuture.allOf(future1, future2, future3);

// ...

combinedFuture.get();

assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

のリターンタイプに注意してください CompletableFuture.allOf() CompletableFuture 。 このメソッドの制限は、すべてのFuturesの結合された結果を返さないことです。 代わりに、Futuresから手動で結果を取得する必要があります。 幸い、 CompleteableFuture.join()メソッドとJava 8 Streams APIを使用すると、次のように簡単になります。

String combined = Stream.of(future1, future2, future3)
  .map(CompletableFuture::join)
  .collect(Collectors.joining(" "));

assertEquals("Hello Beautiful World", combined);

CompletableFuture.join()メソッドは get メソッドに似ていますが、 Future が正常に完了しない場合に、チェックされていない例外をスローします。 これにより、 Stream.map()メソッドのメソッド参照として使用できるようになります。

9. エラーの処理

一連の非同期計算ステップでのエラー処理については、 throw /catchイディオムを同様の方法で適応させる必要があります。

構文ブロックで例外をキャッチする代わりに、 CompletableFuture クラスを使用すると、特別なhandleメソッドで例外を処理できます。 このメソッドは、2つのパラメーターを受け取ります。計算の結果(正常に終了した場合)と、スローされた例外(一部の計算ステップが正常に完了しなかった場合)です。

次の例では、 handle メソッドを使用して、名前が指定されていないためにグリーティングの非同期計算がエラーで終了した場合のデフォルト値を提供します。

String name = null;

// ...

CompletableFuture<String> completableFuture  
  =  CompletableFuture.supplyAsync(() -> {
      if (name == null) {
          throw new RuntimeException("Computation error!");
      }
      return "Hello, " + name;
  }).handle((s, t) -> s != null ? s : "Hello, Stranger!");

assertEquals("Hello, Stranger!", completableFuture.get());

別のシナリオとして、最初の例のように、 Future に値を手動で完成させたいが、例外を除いてそれを完成させる機能もあるとします。 completeExceptionally メソッドは、まさにそのためのものです。 次の例のcompletableFuture.get()メソッドは、RuntimeExceptionを原因としてExecutionExceptionをスローします。

CompletableFuture<String> completableFuture = new CompletableFuture<>();

// ...

completableFuture.completeExceptionally(
  new RuntimeException("Calculation failed!"));

// ...

completableFuture.get(); // ExecutionException

上記の例では、 handle メソッドを使用して非同期で例外を処理できましたが、 get メソッドを使用すると、同期例外処理のより一般的なアプローチを使用できます。

10. 非同期メソッド

CompletableFutureクラスのFluentAPIのほとんどのメソッドには、Async接尾辞が付いた2つの追加のバリアントがあります。 これらのメソッドは通常、別のスレッドで対応する実行ステップを実行することを目的としています。

Async 接尾辞のないメソッドは、呼び出しスレッドを使用して次の実行ステージを実行します。 対照的に、Executor引数のないAsyncメソッドは、アクセスされるExecutorの共通のfork/joinプール実装を使用してステップを実行します ForkJoinPool.commonPool()メソッドを使用します。 最後に、Executor引数を持つAsyncメソッドは、渡されたExecutorを使用してステップを実行します。

これは、Functionインスタンスを使用した計算結果を処理する変更された例です。 目に見える唯一の違いはthenApplyAsyncメソッドですが、内部では、関数のアプリケーションは ForkJoinTask インスタンスにラップされています( fork / join [X207X ]フレームワークについては、記事「Javaでのフォーク/結合フレームワークのガイド」を参照してください。 これにより、計算をさらに並列化し、システムリソースをより効率的に使用できます。

CompletableFuture<String> completableFuture  
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future = completableFuture
  .thenApplyAsync(s -> s + " World");

assertEquals("Hello World", future.get());

11. JDK 9 CompletableFuture API

Java 9は、次の変更を加えて CompleteableFutureAPIを拡張します。

  • 新しいファクトリメソッドが追加されました
  • 遅延とタイムアウトのサポート
  • サブクラス化のサポートの改善

および新しいインスタンスAPI:

  • エグゼキュータdefaultExecutor()
  • CompletableFuture newIncompleteFuture()
  • CompletableFuture コピー()
  • CompletionStage minimumCompletionStage()
  • CompletableFuture completeAsync(Supplier <? T>サプライヤー、エグゼキューターエグゼキューターを拡張します)
  • CompletableFuture completeAsync(Supplier <? T>サプライヤーを拡張します)
  • CompletableFuture orTimeout(長いタイムアウト、TimeUnitユニット)
  • CompletableFuture completeOnTimeout(T値、長いタイムアウト、TimeUnit単位)

また、いくつかの静的ユーティリティメソッドがあります。

  • エグゼキュータdelayedExecutor(長い遅延、TimeUnitユニット、エグゼキュータエグゼキュータ)
  • エグゼキュータdelayedExecutor(長い遅延、TimeUnit単位)
  • CompletionStagecompletedStage (U値)
  • CompletionStage failedStage(Throwable ex)
  • CompletableFuture failedFuture(Throwable ex)

最後に、タイムアウトに対処するために、Java9はさらに2つの新しい関数を導入しました。

  • orTimeout()
  • completeOnTimeout()

詳細については、次の記事を参照してください: Java 9CompletableFutureAPIの改善

12. 結論

この記事では、CompletableFutureクラスのメソッドと一般的なユースケースについて説明しました。

この記事のソースコードは、GitHubから入手できます。