[Java] Streamと例外処理は相性が悪いという話
はじめに
ミスリードを誘いそうなタイトルですが、久しぶりに仕事で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は非常に便利ですが、前述したような例外処理が必要なケースなどでは、逆に可読性や保守性を失ってしまう場合もあります。
やはり開発においては、ミーハーになるのも良いですが、常に仕様ありきの実装ではなく、実装ありきの仕様選択からできるようになりたいものですね。
注:本記事のコードは途中から直書きなので、そのままコンパイルしたら通らないかもしれません。