CompletableFutureへのガイド

CompletableFutureへのガイド

1. 前書き

この記事は、CompletableFutureクラスの機能とユースケースのガイドです– Java 8 ConcurrencyAPIの改善として導入されました。

参考文献:

実行可能vs. Javaで呼び出し可能

JavaのRunnableインターフェイスとCallableインターフェイスの違いを学びます。

java.util.concurrent.Futureへのガイド

java.util.concurrent.Futureのガイドとその実装の概要

2. Javaでの非同期計算

非同期計算について考えるのは困難です。 通常、計算は一連のステップとして考えます。 ただし、非同期計算の場合、actions represented as callbacks tend to be either scattered across the code or deeply nested inside each other。 いずれかのステップで発生する可能性のあるエラーを処理する必要がある場合、事態はさらに悪化します。

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

In Java 8, the CompletableFuture class was introduced.Futureインターフェイスに加えて、CompletionStageインターフェイスも実装しました。 このインターフェイスは、他のステップと組み合わせることができる非同期計算ステップのコントラクトを定義します。

CompletableFutureは、同時に、about 50 different methods for composing, combining, executing asynchronous computation steps and handling errorsを備えたビルディングブロックおよびフレームワークです。

このような大きなAPIは圧倒的ですが、これらは主にいくつかの明確で明確なユースケースに分類されます。

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

まず、CompletableFutureクラスはFutureインターフェースを実装しているので、use it as a Future implementation, but with additional completion logicを実行できます。

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

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

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

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

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

    return completableFuture;
}

計算をスピンオフするには、記事“Introduction to Thread Pools in Java”で説明されているExecutor APIを使用しますが、CompletableFutureを作成して完了するこの方法は、任意の同時実行メカニズムまたはAPIと一緒に使用できます。生のスレッドを含みます。

the calculateAsync method returns a Future instanceに注意してください。

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

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

Future completableFuture = calculateAsync();

// ...

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

If you already know the result of a computationの場合、この計算の結果を表す引数を使用して静的completedFutureメソッドを使用できます。 その場合、Futuregetメソッドはブロックされず、代わりにこの結果をすぐに返します。

Future completableFuture =
  CompletableFuture.completedFuture("Hello");

// ...

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

別のシナリオとして、cancel the execution of a Futureが必要な場合があります。

結果を見つけることができず、非同期実行を完全にキャンセルすることにしたとします。 これは、Futurecancelメソッドを使用して実行できます。 このメソッドはboolean引数mayInterruptIfRunningを受け取りますが、CompletableFutureの場合、割り込みはCompletableFutureの処理を制御するために使用されないため、効果はありません。

非同期メソッドの修正バージョンは次のとおりです。

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

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.cancel(false);
        return null;
    });

    return completableFuture;
}

Future.get()メソッドを使用して結果をブロックすると、futureがキャンセルされると、CancellationExceptionがスローされます。

Future future = calculateAsyncWithCancellation();
future.get(); // CancellationException

4. カプセル化された計算ロジックを使用したCompletableFuture

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

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

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

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

Supplierインターフェースは、引数を持たず、パラメーター化された型の値を返す単一のメソッドを持つジェネリック関数型インターフェースです。

これにより、provide an instance of the Supplier as a lambda expression that does the calculation and returns the resultが可能になります。 これはとても簡単です。

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

// ...

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

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

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

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

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

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

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

CompletableFutureには、このユースケースのメソッドがあります。thenAcceptメソッドはConsumerを受け取り、計算結果を渡します。 最後のfuture.get()呼び出しは、Voidタイプのインスタンスを返します。

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

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

future.get();

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

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

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

future.get();

6. 先物の組み合わせ

CompletableFuture APIの最良の部分は、ability to combine CompletableFuture instances in a chain of computation stepsです。

この連鎖の結果は、それ自体がCompletableFutureであり、さらなる連鎖と結合を可能にします。 このアプローチは、関数型言語ではどこにでもあり、多くの場合、単項設計パターンと呼ばれます。

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

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

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

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

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

どちらのメソッドも関数を受け取り、それを計算結果に適用しますが、thenComposeflatMap)メソッドはreceives a function that returns another object of the same typeです。 この機能構造により、これらのクラスのインスタンスをビルディングブロックとして構成できます。

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

CompletableFuture 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も、異なるCompletableFuture呼び出しを連鎖させるのに役立ちますが、これら2つの関数の使用法は異なります。

7.1. thenApply()

This method is used for working with a result of the previous call.ただし、覚えておくべき重要な点は、戻り値の型がすべての呼び出しで結合されることです。

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

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

7.2. thenCompose()

thenCompose()メソッドは、両方とも新しい完了ステージを返すという点でthenApply()に似ています。 ただし、thenCompose() uses the previous stage as the argumentthenApply():で観察されたようにネストされた未来ではなく、フラット化して結果を直接返すFutureを返します

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

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

また、これら2つの方法の違いは、https://www.example.com/java-difference-map-and-flatmap.に類似していることに注意してください。

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

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

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

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

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

// ...

combinedFuture.get();

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

CompletableFuture.allOf()の戻り値の型がCompletableFuture<Void>であることに注意してください。 このメソッドの制限は、すべてのFuturesの合計結果を返さないことです。 代わりに、Futuresから手動で結果を取得する必要があります。 幸い、CompletableFuture.join()メソッドとJava 8 StreamsAPIを使用すると簡単になります。

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 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 completableFuture = new CompletableFuture<>();

// ...

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

// ...

completableFuture.get(); // ExecutionException

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

10. 非同期メソッド

CompletableFutureクラスのFluent APIのほとんどのメソッドには、Asyncの接尾辞が付いた2つの追加のバリアントがあります。 これらのメソッドは通常、running a corresponding step of execution in another threadを対象としています。

Async後置がないメソッドは、呼び出しスレッドを使用して次の実行ステージを実行します。 Executor引数のないAsyncメソッドは、ForkJoinPool.commonPool()メソッドでアクセスされるExecutorの一般的なfork/joinプール実装を使用してステップを実行します。 Executor引数を持つAsyncメソッドは、渡されたExecutorを使用してステップを実行します。

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

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

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

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

11. JDK 9CompletableFuture API

Java 9では、CompletableFuture APIがさらに拡張され、次の変更が加えられました。

  • 新しいファクトリーメソッドが追加されました

  • 遅延とタイムアウトのサポート

  • サブクラス化のサポートの改善。

新しいインスタンスAPIが導入されました。

  • エグゼキュータdefaultExecutor()

  • CompletableFuture newIncompleteFuture()

  • CompletableFuture copy()

  • CompletionStage minimumCompletionStage()

  • CompletableFuture completeAsync(Supplier <? T>サプライヤー、エグゼキューターエグゼキューターを拡張します)

  • CompletableFuture completeAsync(Supplier <? T>サプライヤーを拡張)

  • CompletableFuture またはTimeout(長いタイムアウト、TimeUnit単位)

  • CompletableFuture completeOnTimeout(T値、長いタイムアウト、TimeUnit単位)

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

  • エグゼキュータdelayedExecutor(長い遅延、TimeUnitユニット、エグゼキュータエグゼキュータ)

  • エグゼキュータdelayedExecutor(長い遅延、TimeUnit単位)

  • CompletionStage completeStage(U value)

  • CompletionStage failedStage(Throwable ex)

  • CompletableFuture failedFuture(Throwable ex)

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

  • orTimeout()

  • completeOnTimeout()

詳細については、次の記事をご覧ください:Java 9 CompletableFuture API Improvements

12. 結論

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

記事のソースコードはover on GitHubで入手できます。