Java で非同期処理 for Android ：CompletableFuture 入門


# はじめに

非同期処理は、モダンなAndroidアプリケーション開発において欠かせない技術の一つです。アプリがユーザーの操作に対して素早く反応し、スムーズな体験を提供するためには、バックグラウンドでの作業（例: ネットワーク通信やファイル操作など）を効率的に行う必要があります。これを同期的に処理してしまうと、操作が完了するまでアプリ全体が固まったり、ユーザーに「フリーズしている」という印象を与えることになります。

非同期処理を正しく使うことで、こうした問題を回避し、ユーザーが操作中でもアプリがスムーズに動作し続けるように設計できます。特にAndroidアプリでは、メインスレッド（UIスレッド）で重たい処理を行うと、アプリが応答しなくなる恐れがあり、最悪の場合、システムから「アプリが応答していません」（ANR: Application Not Responding）エラーを出されてしまいます。

ここで役立つのが、Javaの標準ライブラリで提供されている **`CompletableFuture`** です。`CompletableFuture` は、Java 8 から導入された強力な非同期処理ツールで、手軽に非同期タスクを実行し、結果を処理することができます。`Thread` クラスや `AsyncTask` 、`ExecutorService`といった従来の非同期処理と比べて、よりシンプルかつ柔軟に非同期処理を行えるのが特徴です。

本記事では、`CompletableFuture` を使ってAndroidアプリで非同期処理を簡単に実装する方法を解説します。初めて非同期処理に触れる方でも、この記事を読み終わる頃には、`CompletableFuture` を使ってバックグラウンドで処理を実行し、その結果をメインスレッドに反映させる基本的な操作ができるようになります。

# CompletableFuture の基本操作

ここでは、`CompletableFuture` を使った非同期処理の基本的な操作を具体的に説明します。非同期タスクの実行から、その完了時の処理をシンプルに書く方法を見ていきましょう。特に、`supplyAsync` と `whenComplete` の使い方に焦点を当てます。

## `supplyAsync` で非同期タスクを実行する

`CompletableFuture.supplyAsync` メソッドを使って、バックグラウンドで非同期にタスクを実行できます。タスクは別スレッドで処理され、メインスレッド（UIスレッド）に負荷をかけません。

次のコードでは、バックグラウンドで重い処理を実行しています。

```java
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // バックグラウンドで実行される重い処理
    try {
        Thread.sleep(2000); // 2秒間の処理
    } catch (InterruptedException e) {
        throw new RuntimeException("処理が中断されました", e);
    }
    return "非同期処理完了";
});
```

`CompletableFuture.supplyAsync` はラムダ式を引数に取り、その中に実行したい処理を記述します。ここでは2秒の遅延をシミュレートしています。エラーが発生した場合は例外をスローします。

ここで重要なのは、`CompletableFuture.supplyAsync`の呼び出し自体はすぐにリターンするということです。 この呼び出しにより別スレッド上で引数として渡した処理の実行が開始されます。実行するスレッドは、Java実行環境が管理する共有スレッドプールから選択されるため、明示的にスレッドを生成する必要はありません。

## `whenComplete` でタスク完了時の処理を行う

`whenComplete` メソッドを使って、非同期タスクが完了した際の処理を実行できます。このメソッドは、タスクの結果と、エラー発生時の例外を受け取り、それに応じて処理を行います。

次のコードは、非同期タスクの完了後に結果をログに表示する例です。

```java
future.whenComplete((result, exception) -> {
    if (exception == null) {
        // 正常にタスクが完了した場合の処理
        System.out.println("タスク成功: " + result);
    } else {
        // エラーが発生した場合の処理
        System.out.println("エラー発生: " + exception.getMessage());
    }
});
```

この例では、`whenComplete` メソッドで成功時には結果を表示し、エラー発生時にはエラーメッセージを表示しています。これにより、タスクの成功・失敗を分岐して処理できるようになります。

ここで重要なのは、`whenComplete` の呼び出し自体はすぐにリターンするということです。`whenComplete`の引数として渡したタスクは、`supplyAsync`で引数に指定した非同期タスクの完了後に実行されます。

## `supplyAsync` 内でのエラー処理

非同期タスクの実行中にエラーが発生するケースも考慮しましょう。例えば、`supplyAsync` 内で例外がスローされた場合、そのエラーも `whenComplete` で処理することができます。以下は、その例です。

```java
CompletableFuture<String> futureWithError = CompletableFuture.supplyAsync(() -> {
    // バックグラウンドでの処理中にエラーを発生させる
    if (true) {
        throw new RuntimeException("意図的なエラー");
    }
    return "非同期処理完了";
});

futureWithError.whenComplete((result, exception) -> {
    if (exception == null) {
        // 正常にタスクが完了した場合
        System.out.println("タスク成功: " + result);
    } else {
        // エラーが発生した場合
        System.out.println("エラー発生: " + exception.getMessage());
    }
});
```

このコードでは、`supplyAsync` の中で意図的に例外をスローし、`whenComplete` でその例外をキャッチして処理しています。このようにして、非同期処理の中でエラーが発生した場合でも、それを適切に扱うことができます。
エラーハンドリングを含めて、非同期処理の流れを理解できるようになると、アプリがより堅牢になり、ユーザーにスムーズな体験を提供できるようになります。

# CompletableFuture の実装例

## DataFetcher クラスの実装

```java
public class DataFetcher {

    // 非同期でサーバーからデータを取得するメソッド
    public CompletableFuture<String> fetchDataFromServer() {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // サーバーからのデータ取得処理（ダミー）
                Thread.sleep(2000); // 2秒間の遅延をシミュレート
            } catch (InterruptedException e) {
                throw new RuntimeException("データ取得が中断されました", e);
            }
            return "サーバーからのデータ";
        });
    }
}
```

## Activity からの呼び出し


```java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.textView);
        Button fetchButton = findViewById(R.id.fetchButton);
        DataFetcher dataFetcher = new DataFetcher();

        // ボタンを押したときに非同期でデータを取得
        fetchButton.setOnClickListener(view -> {
            dataFetcher.fetchDataFromServer().whenComplete((result, exception) -> {
                // メインスレッドでUIを更新
                runOnUiThread(() -> {
                    if (exception == null) {
                        textView.setText("取得したデータ: " + result);
                    } else {
                        textView.setText("エラー発生: " + exception.getMessage());
                    }
                });
            });
        });
    }
}
```



## 解説

1. **`DataFetcher` クラスの使用**  
   `DataFetcher` クラスは、非同期でサーバーからデータを取得するロジックを担当します。これにより、`MainActivity` にはビジネスロジックが含まれず、責務が分離されている状態です。

2. **`onCreate` での呼び出し**  
   `onCreate` 内で `fetchButton` がクリックされた際に、`DataFetcher` の `fetchDataFromServer` メソッドを直接呼び出して、非同期で処理を実行します。

3. **UI の更新**  
   `whenComplete` 内で結果を取得した後、`runOnUiThread` を使って `TextView` を更新します。これにより、バックグラウンド処理の結果を メインスレッドで受け取り、UIに反映させることができます。


このシンプルな実装例では、ボタンを押したときに非同期でサーバーからデータを取得し、その結果を画面に表示します。DataFetcher クラスにビジネスロジックを分けることで、Activity はUIの制御に専念でき、非同期処理も簡潔に管理できます。この方法は、メインスレッドをブロックせず、アプリの応答性を維持しながら非同期タスクを実装する上で有効です。

# CompletableFuture を使った非同期処理の利点

## 従来の非同期処理との比較

Androidアプリ開発では、非同期処理を行うために従来から `AsyncTask` や `Handler`、`HandlerThread`、 `ExecutorService`クラスが使用されてきました。これらの方法には次のような問題がありました：

1. **`AsyncTask` のデメリット**  
   `AsyncTask` は、非同期処理とメインスレッドへの結果の受け渡しを比較的簡単に実装できるものの、構造が複雑になりやすく、コードが煩雑になる傾向があります。また、`AsyncTask` は Android API 30 で廃止されるため、新しいプロジェクトでは推奨されません。

2. **`Handler` や `HandlerThread` のデメリット**  
   `Handler` や `HandlerThread` を使用して非同期処理を行う場合、明示的にスレッドを作成し、そのスレッドに対して Handler を用いてメッセージを送信する実装になります。非同期で処理した結果の受け取りやエラーハンドリングを行うために、煩雑なコードが必要になります。処理の流れも追いにくくなります。また、スレッドの終了処理を忘れるとメモリリークに繋がります。

3. **`ExecutorService`のデメリット**
  JavaにおけるFutureパターンの実装として`ExecutorService.submit`を使う例がありますが、ここで使われる`Future`クラスは、`CompletableFuture.whenComplete` に相当するメソッドを持たず、`Future.get`で値を受け取る必要があるため、結局メインスレッドをブロックすることになってしまいます。また、`Handler` の時と同様に、スレッドの管理を行う必要があり、終了処理を忘れるとメモリリークに繋がります。

## CompletableFuture の利点

`CompletableFuture` を使用すると、従来の方法に比べて以下の利点があります。

1. **スレッド管理の簡便さ**  
   `CompletableFuture` を使うと、内部でスレッドプールを管理してくれるため、開発者がスレッドを直接扱う必要がありません。これにより、複雑なスレッド管理が不要になり、コードがシンプルに保たれます。

1. **エラーハンドリングの一貫性**  
   `whenComplete` や `exceptionally` メソッドを使えば、エラーが発生した場合でも簡単に処理を行えます。従来の方法では、エラーハンドリングが煩雑になりがちですが、`CompletableFuture` では一貫してシンプルに実装できます。

1. **シンプルで読みやすいコード**  
   `CompletableFuture` を使えば、非同期処理が終了した後の動作を `whenComplete` などのメソッドで簡潔に記述できます。コードが自然なフローで書かれるため、可読性が向上します。

   ```java
   dataFetcher.fetchDataFromServer().whenComplete((result, exception) -> {
       if (exception == null) {
           textView.setText(result);
       } else {
           textView.setText("エラー発生");
       }
   });
   ```

   このコード例では、非同期処理の開始から結果の取得、エラーハンドリングまでが一連のフローで書かれており、読みやすくなっています。

1. **柔軟な非同期タスクの組み合わせ**  
   `CompletableFuture` を使えば、複数の非同期タスクを簡単に組み合わせたり、チェーンさせることが可能です。例えば、データの取得が終わった後に別の非同期処理を行いたい場合でも、`thenApply` や `thenCompose` といったメソッドを使って自然な流れで処理を記述できます。

   ```java
   dataFetcher.fetchDataFromServer()
       .thenApply(result -> processResult(result))  // 結果を処理
       .whenComplete((finalResult, exception) -> {
           if (exception == null) {
               textView.setText(finalResult);
           } else {
               textView.setText("エラー発生");
           }
       });
   ```


## 実際のプロジェクトでの使用例

`CompletableFuture` は、小規模なプロジェクトだけでなく、大規模なプロジェクトでも非常に役立ちます。特に、次のようなシナリオで有効です：

- **ネットワーク通信**  
  サーバーからデータを取得し、それに基づいてUIを更新する処理を非同期で行う場合に最適です。

- **データベースアクセス**  
  ローカルのデータベース（例えば、SQLite）にアクセスし、その結果を元に次のアクションを実行する場合にも、非同期処理の組み合わせが役立ちます。

- **複数の非同期処理の組み合わせ**  
  複数のネットワークリクエストやデータ処理を連続して実行する場合でも、`CompletableFuture` のメソッドチェーンを使うことで簡潔に記述できます。


# まとめ

この記事では、Android アプリ開発における非同期処理の基本として、`CompletableFuture` を使ったシンプルな例を紹介しました。`CompletableFuture` は、従来の非同期処理手法よりもシンプルで直感的にコードを記述でき、スレッド管理やエラーハンドリングも簡潔に行える強力なツールです。

- **`CompletableFuture` の基本操作**として、`supplyAsync` を使った非同期処理の実行方法と、`whenComplete` を使った結果処理やエラーハンドリングについて説明しました。
- **ビジネスロジックの分離**を意識して、`DataFetcher` クラスを用いることで、`Activity` からロジックを切り離し、UI の操作と非同期処理をスッキリと実装する手法を紹介しました。
- **Android アプリ開発における利点**として、スレッド管理を意識せずに済むこと、柔軟な非同期処理の組み合わせが可能であることを挙げました。

`CompletableFuture` を活用することで、UI の応答性を損なうことなく、複雑な非同期処理を簡単に管理できるようになります。今回紹介した基礎的な使い方を学ぶことで、実際のプロジェクトでも効果的に非同期処理を扱えるようになるでしょう。ぜひ、積極的に活用してみてください。