Help us understand the problem. What is going on with this article?

CompletableFutureの公式ドキュメントを読んでも分からないので Java逆引きレシピを片手に理解する 

この記事はチームラボエンジニアリングの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メソッドの使い方や、実行されるタイミングが分からなかったのでこれを機会に知れて良かった

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした