slontが2016年4月22日に投稿(2016年5月14日更新)

はじめに

ミスリードを誘いそうなタイトルですが、久しぶりに仕事で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

slont

元金融エンジニア。メイン言語はJava, HTML, JavaScript, Python, Kotlinあたり。SECCONやCTF、NLP、機械学習に興味あり。金融日記購読4年。巷で話題の変態紳士。美女ソムリエ始めてました。 お仕事の依頼はTwitterからお願いします。

comments powered by Disqus
Back to top