本稿について
業務でJava8を使い始めて,Stream APIを使うようになり,つい先日も新人に説明したりする中で,記録に残しておこうと思い,作成したものです.
そのため,まとめた記事という性質上,ほかの方々の記事に似た内容になってしまうことを認識の上,閲覧・参考ください.
Stream APIの基本
基本的な流れ
- Streamを取得
- 中間操作を定義
- 終端操作を定義
中間操作は複数追加でき,追加した順番で評価されていく.
各操作で指定した関数型インタフェイスは,作用する際に呼び出されるため,遅延評価となる.
注意点
Java8では,終端操作を定義して実行されたStreamは再利用できない.
そのため,Streamを使う直前に作成するように心がけると良い.
// 1から10までの乱数を20個取得してそれぞれ出力する
Random r = new Random();
IntStream is = r.ints(20, 1, 10);
// Streamを実行
is.forEach(System.out::println);
// 使用済みStreamを再度実行
is.forEach(System.out::println); // -> IllegalStateException発生
Java9では,再利用できるメソッドが追加されている.
Streamの生成方法
Streamから作る
Stream<T>は任意のオブジェクト型のStreamとなっている.
int/double/longのプリミティブ型については,それぞれ専用のStreamが用意されている.
// String型のStream
Stream<String> ss = Stream.of("One", "Two", "Three", "Four");
// 1から10の値を発生させるint型のStream
IntStream is = IntStream.rangeClosed(1, 10);
// 1.0を40個発生させるdouble型のStream
DoubleStream ds = DoubleStream.generate( () -> 1.0 ).limit(40);
Collection型から作る
ListやSetからStreamを取得できる.
// Listから生成
List<String> strList = Arrays.asList("A", "B", "C", "D");
Stream<String> ss = strList.stream();
// Mapから生成
Map<String, Integer> map = new HashMap<>();
// (Key,Value)の組でStreamを作る
Stream<Map.Entry<String, Integer>> map.entrySet().stream();
BufferedReaderから作る
ファイルを読み込みながら,Streamで処理することができる.
// sample.txtファイルの中にある"#"から始まる行だけを表示する
try (BufferedReader reader = Files.newBufferedReader(Paths.get("sample.txt"))) {
reader.lines()
.filter(line -> line.startsWith("#"))
.forEach(System.out::println);
}
中間操作の種類
ここで挙げるもの以外にも中間操作はあるが,使う機会がなかったflatMap
などは割愛する.
filter(Predicate<T>)
条件に合致したものだけに絞る際に利用する.if文に類似した作用がある.
// 1から10までの乱数を20個取得して,偶数の値だけを出力する
Random r = new Random();
r.ints(20, 1, 10)
.filter(i -> i%2==0)
.forEach(System.out::println);
map(Function<T, R>)
別の型に変換する際に利用する.
変換元 | 変換先 | 利用する中間操作 |
---|---|---|
int | T | IntStream#mapToObj(IntFunction<T>) |
long | T | LongStream#mapToObj(LongFunction<T>) |
double | T | DoubleStream#mapToObj(DoubleFunction<T>) |
T | R(Tと同じでも化) | Stream<T>#map(Function<T, R>) |
T | int | Stream<T>#mapToInt(ToIntFunction<T>) |
T | long | Stream<T>#mapToLong(ToLongFunction<T>) |
T | double | Stream<T>#mapToDouble(ToDoubleFunction<T>) |
limit(int) / skip(int)
limit(int)
は,Streamで処理する上限値を設定する.MySQLのlimit句と同じ作用がある.
skip(int)
は,Streamのうち最初から引数の値の分をスキップさせる.
// 1~10のうち最初の3つだけ出力させる(1~3が出力される)
IntStream.rangeClosed(1, 10).limit(3).forEach(System.out::prinln);
// 1~10のうち最初の3つを飛ばして出力させる(4~10が出力される)
IntStream.rangeClosed(1, 10).skip(3).forEach(System.out::prinln);
peek(Consumer<T>)
途中で値を確認したいときなどに利用(デバッグ目的).
また,BeanやDTOなどの場合,フィールドへの設定などができる(あまり使わないかも?).
sorted() / sorted(Comparator<T>)
ソートさせるときに利用する.
BeanやDTOなどの場合,任意のフィールドでソートしたい場合は,比較子を定義させるsorted(Comparator<T>)
を利用する.
IntStreamやLongStreamには,sorted()
しか存在しない.
sequential() / parallel()
sequential()
は,Streamの順番を維持しながら,後続の操作を行うようにさせる.
parallel()
は,Streamの順番は意識せずに並列的に,後続の操作を行うようにさせる(順番の保証がない).
boxed()
boxed()
は,int型からInteger型やdouble型からDouble型などボクシング(Boxing)するためのもの.
Streamとしては,IntStreamからStream<Integer>やDoubleStreamからStream<Double>に変化する.
その逆の作用を与えるには,mapToInt(ToIntFunction<T>)
やmapToDouble(ToDoubleFunction<T>)
を使う.
distinct()
重複削除する.
終端操作の種類
forEach(Consumer<T>)
Streamで流れてきた要素を1つずつ処理するときに利用する(出力やメソッド呼び出しなど).
count()
Streamで流れてきた数を数える.
collect(Collector<T, A, R>), collect(Supplier<R>, BiConsumer<R, T>, BiConsumer<R, R>)
Streamで流れてきた要素を特定のCollection型や文字列連結など要素を集める時に利用する.
Collectorsについて
collectの引数は2種類あり,Collectorを指定するものと細かく設定できるメソッドとがある.
List<T>
に変換したり,Map<K, V>
に集約させたり,と代表的な処理はCollectorsというクラスにメソッドを用意してある.
よく使いそうなメソッドだけ挙げてあります.
メソッド | 出来ること | collectの戻り値 |
---|---|---|
Collectors.toList() | ArrayListにまとめる | List |
Collectors.toMap(Function<T, K>, Function<T, U>) | 第一引数にKeyへのマッピングルール,第二引数にValueへのマッピングルールをに基づいて,HashMapにまとめる | Map<K, U> |
Collectors.joining() | 文字列を1つにつなげる | String |
Collectors.groupingBy(Function<T, U>) | クラス分けしてHashMapにまとめる | Map<U, List<T>> |
reduce(T, BiFunction<T>)
Streamで流れてきた要素を1つの要素に混ぜ合わせるときに利用する.
collect()
に似た作用を起こすが,reduce()
は合計や累積などの算出をするときに利用する.
toArray(IntFunction<T[]>)
Streamで流れてきた要素を配列にさせるときに利用する.
// 1~10の値を持った配列の生成
int[] intAry = IntStream.rangeClosed(1, 10).toArray(int[]::new);
max(), max(Comparator<T>) / min(), min(Comparator<T>)
Streamで流れてきた要素のうち,最大値(最小値)を取得する際に利用する.
Lambda式について
Streamの各操作の引数に出てくる関数型インタフェイスはLambda式で記述ができるため,すっきりとした処理を書くことができる.
Lambda式自体には少し制約があるため,無理にLambda式にするとかえって読みにくくなることもある.
そして,様々な処理に対応できるように関数型インタフェイスには,あらかじめ様々な種類のものがあるため,一覧にしてまとめておく.
一覧には,int型しか記載しないが,long型とdouble型については型違いだけのため割愛する.
気になる場合は,java.util.function配下にたくさん用意されているため,眺めてみると良い.
Lambda式サンプルの引数は,1つの場合はカッコを省略できるが,記述を統一するためカッコはつけたままにする.
Lambda式で使用する変数について
Lambda式は,変数定義やif文,for文など普通にコーディングができる.
見た目上,メソッド内にあるコードのように見えるため,Lambda式の外にある変数に代入したくなるが,それは不可能である.
Lambda式はコンパイルするとprivate staticなメソッドとして置き換わるため,変数への代入は出来ない.
ListやMapなどのオブジェクトの中にあるフィールドに作用するメソッドの呼び出し(List#addやMap#putなど)は利用できる.
int a = 0;
List<Integer> list = new ArrayList<>();
IntStream.rangeClosed(1, 10)
.forEach( i -> {
list.add(i); // <- これはOK
if(i==6) {
a = 6; // <- コンパイルエラーになる
}
});
検査例外について
Lambda式は,アトミックである処理や単純な処理の場合に最大限の効果が発揮されるため,例外が発生することが前提となる処理をLambda式で無理やりコーディングすることは,目的に反していると思っている.
そのため,例外が発生する場合は,Lambda式内部でtry-catchをする必要がある.
それでもエスカレーションさせたい場合は,UncheckedIOException
などでラッピングする必要がある.
Runnable
戻り値 | 引数 | 対応する関数型インタフェイス | Lambda式サンプル |
---|---|---|---|
void | void | Runnable | () -> System.out.println("Start") |
Consumer<T> / BiConsumer<T, U>
戻り値 | 引数 | 対応する関数型インタフェイス | Lambda式サンプル |
---|---|---|---|
void | int | IntConsumer | (i) -> list.add(i) |
void | T | Consumer<T> | (str) -> stringBuilder.append(str) |
void | (T, U) | BiConsumer<T, U> | (key, value) -> map.put(key, value) |
void | (T, int) | ObjIntConsumer<T> | (map, value) -> map.merge(value, 1, Integer::sum) |
void | (T, T) | BinaryOperator<T> | (a, b) -> a.compareTo(b)<=0 ? a : b |
Supplier<T>
戻り値 | 引数 | 対応する関数型インタフェイス | Lambda式サンプル |
---|---|---|---|
int | void | IntSupplier | () -> 1 |
T | void | Supplier<T> | () -> "A" |
Function<T, R> / BiFunction<T, U, R> / Predicate<T> / BiPredicate<T, U>
戻り値 | 引数 | 対応する関数型インタフェイス | Lambda式サンプル |
---|---|---|---|
int | int | IntUnaryOperator | (i) -> 10 + i |
R | int | IntFunction<R> | (i) -> "no." + i |
double | int | IntToDoubleFunction | (i) -> i * i * Math.PI |
R | T | Function<T, R> | (str) -> "<" + str + ">" |
int | T | ToIntFunction<T> | (str) -> Integer.parseInt(str) |
int | (T, U) | ToIntBiFunction<T, U> | (str1, str2) -> (str1 + str2).length() |
int | (int, int) | IntBinaryOperator | (i, j) -> i % j |
boolean | T | Predicate<T> | (str) -> str.length() > 5 |
boolean | (T, U) | BiPredicate<T, U> | (strL, strR) -> strL.length() < strR.length() |
終わりに
Stream APIは,それまでのfor文をすっきりと読みやすい形に書き直すことができるので,今後も活用していきたいですね.