0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

これまでの記事では、リアクティブプログラミングの考え方や、Reactive Streams・RxJava・Project Reactorの関係を解説してきました。

本記事では、実際にRxJavaを使って動かせる具体的なコード例を通じて、基本操作を1つずつ確認していきます。

「読めば分かるけど、実際どう書くの」というギャップを埋めることが目的です。

環境準備

Mavenを使う場合、pom.xmlに以下の依存関係を追加します。

<dependency>
    <groupId>io.reactivex.rxjava3</groupId>
    <artifactId>rxjava</artifactId>
    <version>3.1.11</version>
</dependency>

Gradleの場合は以下です。

implementation 'io.reactivex.rxjava3:rxjava:3.1.11'

以降のコード例はすべて、import io.reactivex.rxjava3.core.Observable;が前提です(必要に応じて他のimportは適宜補ってください)。


1. Observableを作る・購読する

リアクティブプログラミングの第一歩は、データの発生源であるObservableを作り、それを 購読(subscribe)することです。

1.1 固定の値を流す:Observable.just

import io.reactivex.rxjava3.core.Observable;

public class Example1 {
    public static void main(String[] args) {
        Observable<String> observable = Observable.just("Java", "Kotlin", "Scala");

        observable.subscribe(
            value -> System.out.println("受信: " + value),   // onNext
            error -> System.out.println("エラー: " + error),  // onError
            () -> System.out.println("完了")                  // onComplete
        );
    }
}

実行結果:

受信: Java
受信: Kotlin
受信: Scala
完了

subscribeに渡した3つのラムダ式が、それぞれonNext(値を受け取るたび)・onError(エラー時)・onComplete(完了時)に対応します。

justで渡した値が順番に流れ、最後に自動的に完了通知が送られていることが分かります。

1.2 コレクションから作る:Observable.fromIterable

import java.util.Arrays;
import java.util.List;

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

Observable.fromIterable(numbers)
    .subscribe(n -> System.out.println("数値: " + n));

実行結果:

数値: 1
数値: 2
数値: 3
数値: 4
数値: 5

既存のListや配列を、そのままObservableのストリームとして扱いたい場合に使います。

1.3 自分でイベントを発生させる:Observable.create

より細かく制御したい場合は、createを使って自分でいつ値を送出するかを書けます。

Observable<Integer> observable = Observable.create(emitter -> {
    System.out.println("処理を開始します");
    emitter.onNext(10);
    emitter.onNext(20);
    if (true) {
        emitter.onComplete();
    } else {
        emitter.onError(new RuntimeException("何らかのエラー"));
    }
});

observable.subscribe(
    value -> System.out.println("受信: " + value),
    error -> System.out.println("エラー: " + error.getMessage()),
    () -> System.out.println("完了しました")
);

実行結果:

処理を開始します
受信: 10
受信: 20
完了しました

注目してほしいポイント: 「処理を開始します」が表示されるのはsubscribeを呼んだ後です。

Observableはsubscribeされるまで何も実行しません(これを「遅延実行」と呼びます)。

作った時点では何も起きず、購読されて初めて動き出す、という点が命令的なコードとの大きな違いです。


2. 基本オペレーターでストリームを加工する

2.1 map:値を変換する

Observable.just(1, 2, 3, 4, 5)
    .map(n -> n * n)  // 各値を2乗する
    .subscribe(n -> System.out.println("2乗: " + n));

実行結果:

2乗: 1
2乗: 4
2乗: 9
2乗: 16
2乗: 25

2.2 filter:条件に合う値だけ通す

Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    .filter(n -> n % 2 == 0)  // 偶数だけを通す
    .subscribe(n -> System.out.println("偶数: " + n));

実行結果:

偶数: 2
偶数: 4
偶数: 6
偶数: 8
偶数: 10

2.3 オペレーターを連結する

mapfilterのような複数のオペレーターは、メソッドチェーンで連結できます。

上から下に読むだけで、データがどう加工されていくかが分かるのが、宣言的な書き方の利点です。

Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    .filter(n -> n % 2 == 0)     // 偶数だけに絞り込む
    .map(n -> n * 10)            // 10倍する
    .take(3)                     // 最初の3件だけ取る
    .subscribe(n -> System.out.println("結果: " + n));

実行結果:

結果: 20
結果: 40
結果: 60

3. 複数の非同期処理を合成する:flatMap

実務でよくあるのが、「あるデータを取得したら、そのデータを使って別の非同期処理を呼び出す」というパターンです。

ここでは、ユーザーIDから注文一覧を非同期に取得する処理をシミュレートしてみます。

import java.util.concurrent.TimeUnit;

// ユーザーIDから注文一覧を返す「非同期処理」を模したメソッド
Observable<String> fetchOrders(int userId) {
    return Observable.just("注文A", "注文B")
        .delay(500, TimeUnit.MILLISECONDS)  // 500ms待ってから値が流れる想定
        .map(order -> "ユーザー" + userId + "の" + order);
}

Observable.just(1, 2, 3)               // 3人分のユーザーID
    .flatMap(userId -> fetchOrders(userId))  // 各ユーザーIDに対して非同期処理を呼び出す
    .subscribe(order -> System.out.println(order));

実行結果(実行タイミングにより順序は変わる場合があります):

ユーザー1の注文A
ユーザー2の注文A
ユーザー3の注文A
ユーザー1の注文B
ユーザー2の注文B
ユーザー3の注文B

flatMapは、1つの値を受け取って新しいObservableに変換し、それを平坦化して(ネストさせずに)結果のストリームへ合流させるオペレーターです。

「Observableの中でさらにObservableを呼ぶ」という、非同期処理の連鎖にありがちな入れ子構造を、フラットなメソッドチェーンとして書けます。


4. 複数のストリームを組み合わせる

4.1 merge:複数のストリームを1つに合流させる

Observable<String> stream1 = Observable.just("A1", "A2", "A3");
Observable<String> stream2 = Observable.just("B1", "B2", "B3");

Observable.merge(stream1, stream2)
    .subscribe(value -> System.out.println(value));

実行結果(発生順に混ざって流れる):

A1
A2
A3
B1
B2
B3

4.2 zip:複数のストリームの値を1件ずつペアにする

Observable<String> names = Observable.just("Alice", "Bob", "Carol");
Observable<Integer> ages = Observable.just(25, 30, 35);

Observable.zip(names, ages, (name, age) -> name + "(" + age + "歳)")
    .subscribe(result -> System.out.println(result));

実行結果:

Alice(25歳)
Bob(30歳)
Carol(35歳)

zipは、両方のストリームから同じ順番目の値同士を組み合わせます。

値の数が異なる場合は、少ない方に合わせて打ち切られます。


5. エラーハンドリング

5.1 onErrorReturn:エラー時に代わりの値を返す

Observable.just(4, 2, 0, 8)
    .map(n -> 100 / n)  // 0で割るとArithmeticExceptionが発生する
    .onErrorReturn(error -> -1)  // エラー時は-1を代わりに流す
    .subscribe(
        n -> System.out.println("結果: " + n),
        error -> System.out.println("エラー: " + error)
    );

実行結果:

結果: 25
結果: 50
結果: -1
完了(以降の8は処理されない)

onErrorReturnが呼ばれた時点でストリームは終了扱いになるため、その後に控えていた8は処理されません。

エラー発生後もストリームを継続したい場合は、要素単位でのエラー処理を検討する必要があります。

5.2 retry:エラー時に再試行する

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger attempt = new AtomicInteger(0);

Observable.<String>create(emitter -> {
        int current = attempt.incrementAndGet();
        System.out.println(current + "回目の試行");
        if (current < 3) {
            emitter.onError(new RuntimeException("失敗"));
        } else {
            emitter.onNext("成功");
            emitter.onComplete();
        }
    })
    .retry(2)  // 最大2回まで再試行する
    .subscribe(
        value -> System.out.println("結果: " + value),
        error -> System.out.println("最終的に失敗: " + error)
    );

実行結果:

1回目の試行
2回目の試行
3回目の試行
結果: 成功

retry(2)は、エラー発生時に最大2回まで購読処理そのものをやり直します。
ネットワーク越しの通信など、一時的な失敗が起こりうる処理に有効です。


6. スレッドを制御する:subscribeOnobserveOn

RxJavaでは、「どのスレッドで処理を実行するか」を明示的に指定できます。

GUIアプリケーションでは特に、「重い処理はバックグラウンドスレッドで行い、結果の表示だけメインスレッドに戻す」という使い方が重要になります。

import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.core.Observable;

Observable.just("データ取得開始")
    .map(msg -> {
        System.out.println(Thread.currentThread().getName() + "で実行: " + msg);
        // 重い処理(DB問い合わせやファイル読み込みなどを想定)
        return "処理結果";
    })
    .subscribeOn(Schedulers.io())          // ここから上の処理をIO用スレッドで実行
    .observeOn(Schedulers.single())        // ここから下の処理を別スレッドで受け取る
    .subscribe(result -> {
        System.out.println(Thread.currentThread().getName() + "で受信: " + result);
    });

Thread.sleep(500); // mainスレッドがすぐ終了してしまうのを防ぐための待機(サンプル用)

実行結果の例(スレッド名は環境により変わります):

RxCachedThreadScheduler-1で実行: データ取得開始
RxSingleScheduler-1で受信: 処理結果
  • subscribeOn: Observableが値を生成する側の処理をどのスレッドで行うかを指定する(通常はストリームのどこに書いても1箇所だけ有効)
  • observeOn: それ以降の(このオペレーターより下流の)処理を、指定したスレッドに切り替えて実行する

7. 実践例:検索ボックスのオートコンプリート

最後に、これまでのオペレーターを組み合わせた、実務でよくある例を見てみましょう。

ユーザーが検索ボックスに文字を入力するたびに、API的な検索処理を呼び出す場面を想定します。

素朴に実装すると「1文字打つたびに毎回リクエストが飛ぶ」「前のリクエストの結果が後から返ってきて表示が乱れる」といった問題が起きがちです。

import io.reactivex.rxjava3.subjects.PublishSubject;
import java.util.concurrent.TimeUnit;

// 検索キーワードの入力を表すストリーム(実際はUIのテキスト変更イベント等に相当)
PublishSubject<String> searchInput = PublishSubject.create();

// 検索APIを呼び出す処理を模したメソッド
Observable<String> search(String keyword) {
    System.out.println("[API呼び出し] キーワード: " + keyword);
    return Observable.just(keyword + "の検索結果1", keyword + "の検索結果2")
        .delay(300, TimeUnit.MILLISECONDS);
}

searchInput
    .debounce(300, TimeUnit.MILLISECONDS)  // 300ms入力が止まるまで待つ(連続入力の間引き)
    .distinctUntilChanged()                // 直前と同じキーワードなら無視する
    .filter(keyword -> keyword.length() >= 2)  // 2文字未満は検索しない
    .switchMap(keyword -> search(keyword))     // 新しい入力が来たら古い検索は打ち切る
    .subscribe(result -> System.out.println("表示: " + result));

// ユーザーの入力をシミュレート
searchInput.onNext("J");
searchInput.onNext("Ja");
searchInput.onNext("Jav");
searchInput.onNext("Java");
Thread.sleep(1000); // debounce(300ms)より後に発火させる

実行結果の例:

[API呼び出し] キーワード: Java
表示: Javaの検索結果1
表示: Javaの検索結果2

高速に連続入力されたJJaJavJavaのうち、実際にAPIが呼ばれたのは最後のJavaだけです。

これはdebounce(入力が一定時間止まるまで待つ)とswitchMap(新しい値が来たら古い処理を打ち切って切り替える)の組み合わせによるものです。

この2つのオペレーターの組み合わせは、検索オートコンプリートの定番パターンとしてよく使われます。

PublishSubjectは、Observable(データの発生源)とObserver(受け手)の両方の性質を持つ特殊なクラスで、「外部からのイベントを、Observableのストリームとして流し込む入り口」を作りたい場合によく使われます。

まとめ

セクション 使ったオペレーター・API できること
Observableの作成 justfromIterablecreate 固定値・コレクション・自作ロジックからストリームを作る
基本の加工 mapfiltertake 値の変換・絞り込み・件数制限
非同期処理の連鎖 flatMap 1つの値から新しい非同期処理を呼び出し、平坦化して合成する
複数ストリームの合成 mergezip 複数の発生源を1つに合流、またはペアリングする
エラー処理 onErrorReturnretry エラー時の代替値・再試行
スレッド制御 subscribeOnobserveOn 処理を実行するスレッドを指定する
実践例 debouncedistinctUntilChangedswitchMapPublishSubject 検索オートコンプリートのような、入力に応じた非同期処理の典型パターン

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?