この記事はチームラボエンジニアリングの9日目の記事です。
本記事ではCompletableFutureについて書きます。
背景
CompletableFutureの公式ドキュメントを呼んでも分からないので
Java逆引きレシピを片手に理解する
参考URL 参考書
CompletableFuture 英語版Java11
CompletableFuture 日本語版Java8
Java逆引きレシピ
並行処理の種類、違い
スレッド:スレッド内で行った処理をその後で引き継げない
future:getメソッドの呼び出しがスレッドをブロックしてしまう
completablefuture:呼び出し元がブロックされない
thenApplyとthenAcceptの違い
thenApply ・・・ メソッド内で処理した結果を次のメソッドに引き渡せる
thenAccept ・・・ メソッド内で処理した結果を次のメソッドに引き渡せない
thenApplyの場合
CompletableFuture<String> future1 =
CompletableFuture.supplyAsync(
() -> {
// 時間のかかる処理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "java1";
});
// thenApplyの場合
future1
.thenApply(
res -> {
return res; // resには、future1のreturnで定義した「java1」が入っている
})
.thenApply(
res -> {
return res; // resには1回目のthenApplyの戻り値の「java1」が入っている
})
.thenAccept(
res -> {
System.out.println(res); // resには2回目のthenApplyの戻り値の「java1」が入っており、コンソールに出力される
});
// コンソール出力
// java1
thenApplyの値を次のthenApplyに引き渡せています。
thenAcceptの場合
CompletableFuture<String> future1 =
CompletableFuture.supplyAsync(
() -> {
// 時間のかかる処理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "java1";
});
// thenAcceptの場合
future1.thenAccept(
res -> {
System.out.println(res); // return res;を書いて、次のメソッドに値を渡すことはできない
});
// コンソール出力
// java1
thenAccept()メソッド内にreturn を書くことはできない。
理由は、公式ドキュメントを改めて見てみると分かるのですが
// thenApply
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
// thenAccept
public CompletableFuture <Void> thenAccept(Consumer<? super T> action)
thenAcceptには、戻り値にVoidと書いてあるから、そりゃ次に引き渡せないよね、なるほど
thenCombine とは
このステージ と 指定された他のステージの両方が正常終了した際に実行される新しいCompletionStageを返します
クラスCompletableFuture - thenCombineについて
↓
上記文章をさらに自分でも分かるように解釈すると、、、
このステージ = future2
と
指定された他のステージ = future3
の両方が正常終了した際に
実行される新しいCompletionStage = (String s1, String s2) -> s1 + " " + s2
を返します
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(
() -> {
// 時間のかかる処理
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
return "java1";
}
);
CompletableFuture<String> future2 = future1.thenApply(
s -> {
// 時間のかかる処理
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
return "java2";
}
);
CompletableFuture<String> future3 = future1.thenApply(
s -> {
// 時間のかかる処理
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
return "java3";
}
);
// thenCombineにて、future2と第一引数(future3)の両方が正常終了した際に、
// (String s1, String s2) -> s1 + " " + s2を実行している
future2.thenCombine(future3, (String s1, String s2) -> s1 + " " + s2)
.thenAccept(System.out::println);
// コンソール出力
// java2 java3
thenApplyとthenApplyAsyncの違い
コールバックを設定するメソッドには、メソッド名の末尾に「Async」がついたバージョンもある
「Asyncあり」 前のタスクと同一スレッド上で実行される
「Asyncなし」 新たなタスクとしてスレッドが割り当てられる
thenApply ・・・ future1→future3→future2の順番に実行される
thenApplyAsync ・・・ future4実行後に、future5とfuture6が並行に実行される
thenApply
このステージが正常に完了したときに、このステージの結果を指定された関数への引数に設定して実行される新しいCompletionStageを返します。
thenApply
下記のコードだと
future1→future3→future2の順番に実行される
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(
() -> {
// 時間のかかる処理
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
return "java1";
}
);
CompletableFuture<String> future2 = future1.thenApply(
s -> {
// 時間のかかる処理
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
return "java2";
}
);
CompletableFuture<String> future3 = future1.thenApply(
s -> {
// 時間のかかる処理
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
return "java3";
}
);
// future1→future3→future2の順番に実行される
future2.thenCombine(future3, (String s1, String s2) -> s1 + " " + s2)
.thenAccept(System.out::println);
// コンソール出力
// java2 java3
thenApplyAsync
このステージが正常に完了したときに、このステージの結果を指定された関数への引数に設定し、このステージのデフォルトの非同期実行機能を使用して実行される新しいCompletionStageを返します。
thenApplyAsync
自分なりに解釈すると
**このステージ(future4)**が正常に完了したときに、
このステージの結果を
指定された関数()への
**引数(ラムダ式内のsのこと)**に設定し、
このステージのデフォルトの非同期実行機能を使用して
実行される(ラムダ式の中身)
新しいCompletionStageを返します。
下記のコードだと
future4実行後に、future5とfuture6が並列に実行される
CompletableFuture<String> future4 =
CompletableFuture.supplyAsync(
() -> {
// 時間のかかる処理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "java4";
});
CompletableFuture<String> future5 =
future4.thenApplyAsync(
s -> {
// 時間のかかる処理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "java5";
});
CompletableFuture<String> future6 =
future4.thenApplyAsync(
s -> {
// 時間のかかる処理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "java6";
});
// future4実行後に、future5とfuture6が並列に実行される
future5
.thenCombine(future6, (String s1, String s2) -> s1 + " " + s2)
.thenAccept(System.out::println);
// コンソール出力
// java5 java6
CompletableFutureのエラー処理
非同期処理(supplyAsync)が例外で失敗した場合、thenApplyやthenAcceptメソッドで指定したコールバックは呼び出されない
この場合、whenComplete()メソッドやexceptionally()メソッドを使ってハンドリングを行う
whenComplete
exceptionally
thenComposeとthenApplyの違い
これを読むと、通常はthenApplyを使って
thenCompose
このステージが正常に完了したときに、このステージを指定された関数への引数に設定して実行される新しいCompletionStageを返します。
thenApply
このステージが正常に完了したときに、このステージの結果を指定された関数への引数に設定して実行される新しいCompletionStageを返します。
public <U> CompletableFuture<U> thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
公式ドキュメントを読んでも違いが分からない、、、
CompletableFutureを勉強中。thenApplyとthenComposeで混乱中。追記:そしてうらがみさんに色々教えてもらいました!(∩´∀`)∩ワーイ
「thenComposeの使いどころのひとつは、すでにCompletableFutureを返すAPIがあって、それと組み合わせるときかなー、と思いました。」
こちらの記事を参考させていただいて、
すでにCompletableFutureを返すAPIがある場合、thenCompose
新しく作成する場合、thenApply
とのように、基本はthenApplyをつかえば良いのかなとざっくり理解。
まとめ
thenApplyメソッドの使い方や、実行されるタイミングが分からなかったのでこれを機会に知れて良かった