今更ですがJava8らしいコードでプログラミングしたいなーと思ったのでさわりの部分軽くまとめてみます。
Java8の主な新機能
- ラムダ式
- メソッド参照
- Stream API
今回はStream APIについて
ラムダ式を知らない方はまずはラムダ式の記事を見てイメージを持っておいてください。
Stream API
ここでいう「Stream(ストリーム)」とは何でしょうか?
e-wordsによると
ストリームとは、小川、流れ、連続などの意味を持つ英単語で、ITの分野では連続したデータの流れや、データの送受信や処理を連続的に行うことなどを意味する。
Java SE 8のストリームを使用したデータ処理、パート1では
ソースから取得した一連の要素であり、集計操作をサポートするもの
と定義されています。
ちなみにパッケージjava.util.streamでは
集「計」操作を集「約」操作と呼んでいます。
一般的なストリームの操作を
- 問合せの対象となるデータソース(コレクションなど)
- 一連の中間操作(ストリーム・パイプラインを形成)
- 1つの終端操作(ストリーム・パイプラインを実行して結果を生成)
と説明しています。
また
ストリーム・パイプラインを閉じる操作は終端操作と呼ばれます。終端操作は、パイプラインの結果を生成します。
つまり操作の流れは
ソース
↓
ストリーム
↓
中間操作(パイプラインでつなげていく)
↓
終端操作(操作適用)
↓
結果
です。
ここでいう「Stream(ストリーム)」はソースに集約操作(中間操作や終端操作)を連続的に(パイプラインでつなげて)適用できるようにしたものってことですね。
用語やストリーム使用の流れはざっくり説明したのでJava8以前のバージョンの書き方とStream APIの書き方の具体的なコードを比較して見たいと思います。
Java8以前のバージョンの書き方とStream APIの書き方の比較
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test {
public static void main(String[] args) {
List<String> list = Arrays.asList("1", "2", "");
List<Integer> intList = new ArrayList<Integer>();
Pattern p = Pattern.compile("^-?\\d+$");
for (String s : list) {
Matcher m = p.matcher(s);
if (m.find()) { // 正規表現で数値のみにフィルター
int i = Integer.parseInt(s); // 文字列を数値に変更
if (i < 2) { // 条件に合うもののみ追加
intList.add(i);
}
}
}
for (Integer i : intList) { // 残ったものを書き出し
System.out.println(i.toString());
}
}
}
あれこれ入り可読性が悪いです。
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = Arrays.asList("1", "2", "");
list.stream() // ストリームを取得
.filter(s -> s.matches("^-?\\d+$")) // 正規表現で数値のみにフィルター
.mapToInt(s -> Integer.parseInt(s)) // 文字列のストリームを数値のストリームに変更
.filter(n -> (n < 2)) // 条件に合うもののみにフィルター
.forEach( // 残ったものを書き出し
i -> System.out.println(i)
);
}
}
filterで条件がわかりやすく可読性が上がりました。
上で説明したストリーム使用の流れに沿って見ていくと
ストリーム生成
list.stream()
まずは集約操作を適用できるようにストリームへ変換
中間操作
.filter(s -> s.matches("^-?\\d+$")) // 正規表現で数値のみにフィルター
.mapToInt(s -> Integer.parseInt(s)) // 文字列のストリームを数値のストリームに変更
.filter(n -> (n < 2)) // 条件に合うもののみにフィルター
各メソッドがストリームを返してくれるのでパイプラインでつなげます。
-
filterは、1つの引数の述語(boolean値関数)を表すPredicateが引数なので、ラムダ式でboolean返す条件を書きます。
{return ;}の部分は省略しています。 -
mapToIntは、int値の結果を生成する関数を表すToIntFunctionが引数なので、ラムダ式でint値を返すように書きます。
mapToInt以外にもmapToXxという書き方のストリームの型を変換するメソッドがいくつか用意されています。
今回は整数の場合と考えて正規表現でフィルターしましたが
例外で判定するなら以下で書け・・・
.mapToInt(s -> {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
}
return 0;
})
Integerの値を何か返さないといけないから数字でない場合に0返すだなんて
なんて酷いコード・・・
mapToXxでストリームの型を変換をする場合は事前にfilterでの工夫が必要ですね。
終端操作
.forEach( // 残ったものを書き出し
i -> System.out.println(i)
);
Java8から導入されたメソッド参照を使えばさらにシンプルに書けるようになります。
その話はまた後ほど。