Java 8 が登場しました。何か新しいことを学ぶ時が来ました。 Java 7 と Java 6 はわずかに変更されたバージョンですが、Java 8 では大幅な改良が加えられます。 Java 8 は大きすぎるのでしょうか?今日はJDK 8の新しい抽象化CompletableFutureについて徹底解説します。ご存知のとおり、Java 8 は 1 年以内にリリースされるため、この記事はラムダ サポートを備えた JDK 8 ビルド 88 に基づいています。 CompletableFuture は Future を拡張し、メソッド、単項演算子を提供し、古いバージョンの Java に留まらない非同期性とイベント駆動型プログラミング モデルを促進します。 CompletableFuture の JavaDoc を開いたら衝撃を受けるでしょう。約 50 のメソッド (!) があり、その中には非常に興味深いものもありますが、理解するのが難しいものもあります。たとえば、次のとおりです。
次のようにコードをコピーします。 public <U,V> CompletableFuture<V> thenCombineAsync(
CompletableFuture<? は U> を拡張します。
BiFunction<? スーパー T、? スーパー U、?
執行者 執行者)
心配しないで、読み続けてください。 CompletableFuture は、Guava および SettableFuture の ListenableFuture のすべての特性を収集します。さらに、組み込みのラムダ式により、Scala/Akka の将来に近づけられます。本当だとするにはうますぎるように聞こえるかもしれませんが、読み続けてください。 CompletableFuture には、ol における Future の非同期コールバック/変換よりも優れた 2 つの主な側面があり、CompletableFuture の値をいつでもどのスレッドからでも設定できます。
1. パッケージの値を抽出して変更します。
多くの場合、Future は他のスレッドで実行されているコードを表しますが、常にそうとは限りません。 JMS メッセージの到着など、何が起こるかを知っていることを示すために Future を作成したい場合があります。つまり、Future はありますが、将来的に非同期作業が行われる可能性はありません。イベントによって駆動される将来の JMS メッセージが到着したときに、単純に完了 (解決) したいだけです。この場合、単純に CompletableFuture を作成してクライアントに返すことができ、結果が利用可能であると思われる限り、単純に complete() で Future を待っているすべてのクライアントのロックを解除します。
まず、新しい CompletableFuture を作成してクライアントに渡すだけです。
次のようにコードをコピーします。 public CompletableFuture<String> ask() {
Final CompletableFuture<String> future = new CompletableFuture<>();
//...
未来を返す。
}
この Future は Callable とは関係がなく、スレッド プールもなく、非同期的に動作しないことに注意してください。クライアント コードが ask().get() を呼び出すと、永久にブロックされます。レジスタがコールバックを完了しても、有効になることはありません。それで、鍵は何ですか?これで、次のように言えます。
次のようにコードをコピーします: future.complete("42")
...この時点で、すべてのクライアント Future.get() は文字列の結果を取得し、コールバックの完了後すぐに有効になります。これは、Future のタスクを表現したい場合に非常に便利で、実行スレッドのタスクを計算する必要がありません。 CompletableFuture.complete() は 1 回のみ呼び出すことができ、それ以降の呼び出しは無視されます。ただし、新しい Future の前の値を上書きする CompletableFuture.obtrudeValue(...) というバックドアもあるので、使用には注意してください。
Future オブジェクトがそれに含まれる結果または例外を処理できることがわかっているため、信号が失敗したときに何が起こるかを確認したい場合があります。さらにいくつかの例外を渡したい場合は、CompletableFuture.completeExceptionally(ex) を使用できます (または、obtrudeException(ex) のようなより強力なメソッドを使用して前の例外をオーバーライドします)。 completeExceptionally() も待機中のすべてのクライアントのロックを解除しますが、今回は get() から例外がスローされます。 get() について言えば、エラー処理に微妙な変更を加えた CompletableFuture.join() メソッドもあります。しかし、全体的にはどれも同じです。最後に、CompletableFuture.getNow(valueIfAbsent) メソッドがあります。これはブロックはしませんが、Future がまだ完了していない場合はデフォルト値を返します。これは、あまり長く待機したくない堅牢なシステムを構築するときに非常に役立ちます。
最後の静的メソッドは、completeFuture(value) を使用して完成した Future オブジェクトを返すことです。これは、一部のアダプター層をテストまたは作成するときに非常に役立ちます。
2. CompletableFuture を作成して取得する
では、CompletableFuture を手動で作成するのが唯一の選択肢でしょうか?不確かな。通常の Future と同様に、既存のタスクを関連付けることができ、CompletableFuture はファクトリー メソッドを使用します。
次のようにコードをコピーします。
静的 <U> CompletableFuture<U> SupplyAsync(Supplier<U> サプライヤー);
static <U> CompletableFuture<U> SupplyAsync(Supplier<U> サプライヤー、Executor エグゼキューター);
static CompletableFuture<Void> runAsync(Runnable runnable);
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
パラメーターのないメソッド Executor は...Async で終わり、ForkJoinPool.commonPool() (JDK8 で導入されたグローバルな共通プール) を使用します。これは CompletableFuture クラスのほとんどのメソッドに適用されます。 runAsync() は理解するのが簡単ですが、Runnable が必要であるため、Runnable が値を返さないため CompletableFuture<Void> を返すことに注意してください。非同期操作を処理して結果を返す必要がある場合は、Supplier<U> を使用します。
次のようにコードをコピーします。
Final CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@オーバーライド
public String get() {
//...実行時間が長い...
「42」を返します。
}
}、実行者);
ただし、Java 8 にはラムダ式があることを忘れないでください。
次のようにコードをコピーします。
FinalCompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
//...実行時間が長い...
「42」を返します。
}、実行者);
または:
次のようにコードをコピーします。
最終的な CompletableFuture<String> 未来 =
CompletableFuture.supplyAsync(() -> longRunningTask(params)、エグゼキュータ);
この記事はラムダについての記事ではありませんが、私はラムダ式を頻繁に使用します。
3. CompletableFuture(thenApply) での変換とアクション
CompletableFuture の方が Future よりも優れていると言いましたが、その理由がわかりませんか?簡単に言えば、CompletableFuture はアトムであり因子であるためです。私が言ったことは役に立ちませんでしたか? Scala と JavaScript ではどちらも、フューチャーの完了時に非同期コールバックを登録できるため、準備ができるまで待ってブロックする必要がありません。簡単に言うと、「この関数を実行すると、結果が表示されます」ということです。さらに、これらの関数をスタックしたり、複数の Future を組み合わせたりすることもできます。たとえば、String から Integer に変換する場合、関連付けなしで CompletableFuture から CompletableFuture<Integer に変換できます。これは thenApply() によって行われます。
次のようにコードをコピーします。
<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn);
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn);
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor);<p></p>
<p>前述したように、Async バージョンは CompletableFuture でほとんどの操作を提供するため、後のセクションでは省略します。最初のメソッドは、future が完了する同じスレッドでメソッドを呼び出しますが、残りの 2 つは異なるスレッド プールで非同期にメソッドを呼び出します。
thenApply() のワークフローを見てみましょう。</p>
<p><pre>
CompletableFuture<String> f1 = //...
CompletableFuture<Integer> f2 = f1.thenApply(Integer::parseInt);
CompletableFuture<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</p>
またはステートメントで次のようにします。
次のようにコードをコピーします。
CompletableFuture<Double> f3 =
f1.thenApply(Integer::parseInt).thenApply(r -> r * r * Math.PI);
ここでは、文字列から整数、倍精度浮動小数点数へのシーケンスの変換が表示されます。しかし、最も重要なことは、これらの変換はすぐには実行されず、停止されないということです。これらの変換はすぐには実行されず、停止もされません。彼らは、元の f1 が完了したときに実行したプログラムを覚えているだけです。特定の変換に非常に時間がかかる場合は、独自の Executor を提供して非同期に実行できます。この操作は、Scala の単項マップと同等であることに注意してください。
4. 完成したコードを実行します (その後、Accept/thenRun)
次のようにコードをコピーします。
CompletableFuture<Void> thenAccept(Consumer<? super T> ブロック);
CompletableFuture<Void> thenRun(実行可能なアクション);
将来のパイプラインには、2 つの典型的な「最終」ステージ手法があります。これらは、future の値を使用するときに準備されます。 thenAccept() が最終値を提供すると、thenRun は値を計算する方法さえ持たない Runnable を実行します。例えば:
次のようにコードをコピーします。
future.thenAcceptAsync(dbl -> log.debug("Result: {}", dbl), executor);
log.debug("継続中");
...非同期変数は、暗黙的実行者と明示的実行者の 2 つの方法でも使用できますが、この方法についてはあまり強調しません。
thenAccept()/thenRun() メソッドは (明示的な実行者がない場合でも) ブロックしません。これらはイベント リスナー/ハンドラーのようなもので、Future に接続すると一定期間実行されます。未来はまだ完了していませんが、すぐに「継続中」のメッセージが表示されます。
5. 単一の CompletableFuture のエラー処理
ここまでは計算結果についてのみ説明してきました。例外についてはどうですか?それらを非同期的に処理できますか?確かに!
次のようにコードをコピーします。
CompletableFuture<String> セーフ =
future.Exceptionally(ex -> "問題が発生しました: " + ex.getMessage());
Exceptionly() が関数を受け入れると、元の Future が呼び出されて例外がスローされます。この例外を Future 型と互換性のある値に変換して回復する機会があります。 safeFurther 変換では例外は発生しなくなり、代わりに機能を提供する関数から String 値が返されます。
より柔軟なアプローチは、handle() が正しい結果または例外を受け取る関数を受け入れることです。
次のようにコードをコピーします。
CompletableFuture<Integer> 安全 = future.handle((ok, ex) -> {
if (ok != null) {
戻り値 Integer.parseInt(ok);
} それ以外 {
log.warn("問題"、例);
-1 を返します。
}
});
handle() は常に呼び出され、結果と例外は null 以外になります。これは、ワンストップの万能戦略です。
6. 2 つの CompletableFuture を結合する
CompletableFuture は非同期プロセスの 1 つとして優れていますが、複数のこのような Future をさまざまな方法で組み合わせると、それがいかに強力であるかを実際に示します。
7. これら 2 つの Future を結合 (リンク) します (thenCompose())
場合によっては、Future の値を (準備ができたときに) 実行したいことがありますが、この関数は Future も返します。 CompletableFuture は、CompletableFuture<CompletableFuture> と比較して、関数の結果がトップレベルの Future として使用されるべきであることを理解できるほど柔軟です。 thenCompose() メソッドは Scala の flatMap と同等です。
次のようにコードをコピーします。
<U> CompletableFuture<U> thenCompose(Function<? super T,CompletableFuture<U>> fn);
...次の例では、thenApply()(map) と thenCompose()( flatMap) の型と違いを注意深く観察してください。
次のようにコードをコピーします。
CompletableFuture<Document> docFuture = //...
CompletableFuture<CompletableFuture<Double>> f =
docFuture.thenApply(this::calculateRelevance);
CompletableFuture<Double> 関連性Future =
docFuture.thenCompose(this::calculateRelevance);
//...
private CompletableFuture<Double> CalculateRelevance(Document doc) //...
thenCompose() は、中間ステップをブロックしたり待機したりすることなく、堅牢な非同期パイプラインを構築できる重要なメソッドです。
8. 2つの先物の換算値(thenCombine())
thenCompose() を使用して、別の thenCombine に依存する Future をチェーンする場合、両方が完了すると、2 つの独立した Future が結合されます。
次のようにコードをコピーします。
<U,V> CompletableFuture<V> thenCombine(CompletableFuture<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
...2 つの CompletableFuture があり、1 つは Customer をロードし、もう 1 つは最近の Shop をロードすると仮定すると、非同期変数も使用できます。これらは互いに完全に独立していますが、完成したら、それらの値を使用してルートを計算する必要があります。以下に剥奪される例を示します。
次のようにコードをコピーします。
CompletableFuture<Customer> customerFuture =loadCustomerDetails(123);
CompletableFuture<Shop> shopFuture = CloseShop();
CompletableFuture<Route> ルートフューチャー =
customerFuture.thenCombine(shopFuture, (cust, shop) -> findRoute(cust, shop));
//...
private Route findRoute(Customer customer, Shop shop) //...
Java 8 では、 this::findRoute メソッドへの参照を (cust, shop) -> findRoute(cust, shop) に置き換えることができることに注意してください。
次のようにコードをコピーします。
customerFuture.thenCombine(shopFuture, this::findRoute);
ご存知のとおり、customerFuture と shopFuture があります。次に、routeFuture がそれらをラップし、完了するまで「待機」します。準備が完了すると、すべての結果を結合するために提供した関数 (findRoute()) が実行されます。このrouteFutureは、2つの基本的なfutureが完了し、findRoute()も完了すると完了します。
9. すべての CompletableFuture が完了するまで待ちます
これら 2 つの結果を接続する新しい CompletableFuture を生成する代わりに、完了時に通知を受け取りたい場合は、thenAccept Both()/runAfter Both() 一連のメソッドを使用できます (...非同期変数も使用できます)。これらは thenAccept() および thenRun() と同様に機能しますが、1 つではなく 2 つの Future を待ちます。
次のようにコードをコピーします。
<U> CompletableFuture<Void> thenAccept Both(CompletableFuture<? extends U> other, BiConsumer<? super T,? super U> ブロック)
CompletableFuture<Void> runAfter Both(CompletableFuture<?> other, 実行可能なアクション)
上の例を想像してください。新しい CompletableFuture を生成する代わりに、いくつかのイベントを送信するか、GUI をすぐに更新したいだけです。これは簡単に実現できます: thenAccept Both():
次のようにコードをコピーします。
customerFuture.thenAccept Both(shopFuture, (cust, shop) -> {
最終ルート ルート = findRoute(cust, shop);
// ルートを使用して GUI を更新します
});
私が間違っていることを願いますが、おそらく一部の人は、なぜこの 2 つの未来を単純にブロックできないのかと自問するでしょう。 のように:
次のようにコードをコピーします。
Future<顧客> customerFuture =loadCustomerDetails(123);
Future<Shop> shopFuture = CloseShop();
findRoute(customerFuture.get(), shopFuture.get());
もちろん、それもできます。しかし、最も重要な点は、CompletableFuture が非同期を可能にし、ブロックして結果を熱心に待つのではなく、イベント駆動型のプログラミング モデルであるということです。したがって、機能的にはコードの上記 2 つの部分は同等ですが、後者は実行するためにスレッドを占有する必要がありません。
10. 最初の CompletableFuture がタスクを完了するまで待ちます
もう 1 つの興味深い点は、CompletableFutureAPI は (すべてではなく) 最初のフューチャーが完了するまで待機できることです。これは、同じタイプの 2 つのタスクの結果があり、応答時間のみを考慮し、どのタスクも優先しない場合に非常に便利です。 API メソッド (…非同期変数も利用可能):
次のようにコードをコピーします。
CompletableFuture<Void> acceptEither(CompletableFuture<? extends T> other, Consumer<? super T> ブロック)
CompletableFuture<Void> runAfterEither(CompletableFuture<?> other, 実行可能なアクション)
たとえば、統合可能な 2 つのシステムがあるとします。 1 つは平均応答時間は短いですが標準偏差が高く、もう 1 つは一般に遅いですが予測可能です。両方の利点 (パフォーマンスと予測可能性) を最大限に活用するには、両方のシステムを同時に呼び出し、どちらかが先に終了するまで待つことができます。通常、これは最初のシステムですが、進行が遅くなった場合は、2 番目のシステムが許容可能な時間内に完了することがあります。
次のようにコードをコピーします。
CompletableFuture<String> fast = fetchFast();
CompletableFuture<String> 予測可能 = fetchPredictively();
fast.acceptEither(予測可能, s -> {
System.out.println("結果: " + s);
});
s は、fetchFast() または fetchPredictively() から取得した String を表します。私たちは知る必要も気にする必要もありません。
11. 最初のシステムを完全に変換する
applyToEither() は acceptEither() の前身とみなされます。 2 つの Future が完了しようとしているとき、後者はコード スニペットを呼び出すだけで、applyToEither() は新しい Future を返します。これら 2 つの最初の未来が完了すると、新しい未来も完了します。 API は似ています (...非同期変数も使用できます)。
次のようにコードをコピーします:<U> CompletableFuture<U> applyToEither(CompletableFuture<? extends T> other, Function<? super T,U> fn)
この追加の fn 関数は、最初の future が呼び出されたときに完了できます。この特殊なメソッドの目的が何なのかはわかりませんが、結局のところ、fast.applyToEither(predictable).thenApply(fn) を使用するだけで済みます。この API にはこだわっていますが、アプリケーションに追加の機能は実際には必要ないので、単純に Function.identity() プレースホルダーを使用します。
次のようにコードをコピーします。
CompletableFuture<String> fast = fetchFast();
CompletableFuture<String> 予測可能 = fetchPredictively();
CompletableFuture<String> firstDone =
fast.applyToEither(予測可能, Function.<String>identity());
最初に完成した未来を実行することができます。クライアントの観点から見ると、両方のフューチャーが実際には firstDone の背後に隠されていることに注意してください。クライアントは将来のタスクが完了するのを待ち、applyToEither() を使用して最初の 2 つのタスクが完了したときにクライアントに通知します。
12. 複数の組み合わせによる CompletableFuture
これで、2 つの Future が完了するまで (thenCombine() を使用)、最初の Future が完了するまで (applyToEither()) 待つ方法がわかりました。しかし、将来の数に合わせて拡張できるでしょうか?実際、静的ヘルパー メソッドを使用してください。
次のようにコードをコピーします。
静的 CompletableFuture<Void< allOf(CompletableFuture<?<... cfs)
static CompletableFuture<Object< anyOf(CompletableFuture<?<... cfs)
allOf() は、future の配列を使用し、すべての潜在的な future が完了したときに (すべての障害を待機している) future を返します。一方、anyOf() は、最も速い可能性のある先物を待ちます。返される先物の一般的なタイプを見てください。次の記事ではこの問題に焦点を当てます。
要約する
CompletableFuture API 全体を調査しました。これは無敵であると確信しているので、次の記事では、CompletableFuture メソッドと Java 8 ラムダ式を使用した別の単純な Web クローラーの実装について説明します。また、CompletableFuture についても説明します。