[Java] Streamと例外処理は相性が悪いという話

Java 4月 22, 2016

はじめに

ミスリードを誘いそうなタイトルですが、久しぶりに仕事でJavaを書いていた時に、調子に乗ってJava8の新機能を使ってみようと思ってStreamとか試してみたけど、「Stream使えないじゃん!」ってなった時のお話。


そういえばJava9

そういえば、Java9のリリースは来年の3月らしいですね。


Java8で追加されたStreamも、Java9でまた少し強化されるようです。

Java9はモジュール化プロジェクトProject Jigsawが、肥大化した今のJDKに対するアンチテーゼとして注目されていますね。
ただでさえイマドキの言語スタイルとはかけ離れている(それでも取り入れようと頑張ってはいる)Javaですので、これを機に重量級のイメージを払拭して頂きたいですね。


Streamが素敵なところ

さて、何故Streamがダメかの前に、Streamについて、今までのJavaの実装と一緒に簡単におさらいしましょう。


例1

以下のようなリストstrs1を用意します。

List<String> strs1 = new ArrayList<String>() {
    {
        add("apple");
        add("orange");
        add("cherry");
        add("mango");
        add("grape");
    }
};

この要素を1つずつ表示する実装例は以下になります。


Java5以前

# インデックスを指定して取り出す
for (int i=0; i<strs1.size(); i++) {
    System.out.println(strs1.get(i));
}

典型的なインデックス指定型のforループ。


Java5〜Java7

# 拡張for文
for (String str: strs1) {
    System.out.println(str);
}

いわゆる拡張for文。OutOfIndexExceptionの心配を減らしてくれるし、何より見やすいです。


Java8

# アロー演算子
strs1.stream()
     .forEach(str -> System.out.println(str));

# メソッド参照
strs1.stream()
     .forEach(System.out::println);

我らがStreamです。型推論とメソッド参照という強力な武器が使えます。もはやJavaには見えないですね。(ぶっちゃけstream()部分は無くても良いけど、あくまで例です。)



ご覧のとおり、Streamの方が圧倒的に便利ですね!


え?全然良さがわからない?それでは以下はどうでしょう。


例2

List<String> strs2 = new ArrayList<String>() {
    {
        add("apple");
        add("orange");
        add(null);
        add("cherry");
        add("mango");
        add("grape");
        add(null);
    }
};

所々にnullが入ってきました。


そして、処理部分もちょっと工夫します。strs2のnull以外の要素を全て大文字にしたものを、新しくstrs3に追加するという処理を実装してみましょう。


Java7まで

List<String> strs3 = new ArrayList<String>()
for (String str: strs2) {
    if (null == str) {
        continue;
    }
    strs3.add(str.toUpperCase());
}

まあ見慣れた実装ですね。


Java8

List<String> strs3 = strs2.stream() # Streamに変換
     .filter(Objects::nonNull) # null以外をフィルタリング
     .map(String::toUpperCase) # 大文字化
     .collect(Collectors.toList()); # StreamからListへ変換

かなり見通しが良くなったと思います。一つ一つの処理が明確ですし、特にインスタンスの生成部分や、ifでのnull判定は、Streamを使った方がスマートで良いですね。


ワンライナー実装も可能なので、全体的な省エネ化も図れます。


Streamの何がダメなのか

前置きが長くなりましたが、それではこんなStreamの何がダメだったのかの話。


例外処理と相性が悪い

**これに尽きます!!!**Googleで「java stream 例外処理」とかで調べてもらえればわかるんですが、Stream内で検査例外をスロー出来ないため、一旦、非検査例外でラップして投げてから、さらに外側でそれをキャッチして処理しなければなりません。下記のコード例がわかりやすいです。


なので、例えば以下のような実装はできません。

// 例外が出たらその時点でとりあえずエラーとして上に投げたい処理
private void doIt() throws TestException {
    try {
        List<Test> tests = new ArrayList<Test>(){{...}};
        List<Result> results = createResults(tests);
    } catch(Exception e) {
        throw new TestException(e);
    }
}

private List<Result> createResults(List<Test> tests) throw TestException {
    // このようにStream内のcreateResultで投げられたExceptionをcatchできない
    return tests.stream()
                .map(this::createResult)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
}

private Result createResult(Test test) throw TestException {
    if (...) { // なんかの条件ではnullを返す
        return null;
    }
    
    if (...) { // なんかの条件ではTestExceptionを投げる
        throw new TestException(errorMessage);
    }

    return new Return(test);
}

データ処理の途中でエラーが出た場合、とにかくその時点で処理を中断してエラーを投げたいという実装です。しかし、Stream内部で発生した例外を外で取れないため、以下のように書かなければなりません。

private List<Result> createResults(List<Test> tests) throw TestException {
    try {
        return tests.stream()
            .map(test -> {
                try {
                    return createResult(test);
                } catch (TestException e) {
                    throw new RuntimeException(e);
                }
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    } catch (RuntimeException re) {
        throw new TestException(re.getCause());
    }
}

おいおいマジかよといった感じです。これだったら今まで通り拡張for文を使って実装した方が良さそうです。

private List<Result> createResults(List<Test> tests) throw TestException {
    List<Result> results = new ArrayList<Result>();
    for (Test test: tests) {
        Result result = createResult(test);
        if (null != result) {
            results.add(result);
        }
    }
}



おわりに

Streamは非常に便利ですが、前述したような例外処理が必要なケースなどでは、逆に可読性や保守性を失ってしまう場合もあります。


やはり開発においては、ミーハーになるのも良いですが、常に仕様ありきの実装ではなく、実装ありきの仕様選択からできるようになりたいものですね。


注:本記事のコードは途中から直書きなので、そのままコンパイルしたら通らないかもしれません。

slont

金融ベンチャーでWebエンジニア。美と酒とTechで生きてる。Vue.jsが至高。Elixir好き。個人事業とWebアプリ案件もやってます。 アプリ→https://app.cullet.me Android→https://play.google.com/store/apps/details?id=net.maytry.cullet.android

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.