#Stream API #とは
こちらの記事の続きみたいなものです.
Stream API はDataをPipeLine形式で処理するためのAPIです.
Collection,配列,ファイルなどデータの集合体(Data Source)から,個々の要素を取り出して
これを「処理の流れ」(Stream)に引き渡すための仕組みを提供します.
Streamに対して関数操作を行った結果をStreamで返す「中間操作」と
処理結果をDataとして返す「終端操作」があります.
中間操作も終端操作もメソッド引数は関数型インターフェースを取るものが多いため,
ここでラムダ式の知識が利用していくとスマートということになります.
#Stream…?I/O Streamとは違う?
Javaにはjava.ioパッケージにI/O Streamが提供されていますが,
こちらのStreamの意味は入出力をStreamになぞらえた概念ですが,
Stream APIのStreamはDataのPipeLine処理をStreamに見立てた概念です.
#Stream API の基本
import java.util.ArrayList;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// ①Data Sourceを準備する
var list = new ArrayList<String>(
Arrays.asList("tokyo", "nagoya", "osaka", "fukuoka", "hokkaido", "okinawa"));
// ②ストリームを作る
list.stream().
filter(s -> s.length() > 5). // ③中間処理を行う
map(String::toUpperCase).
forEach(System.out::println); // ④終端処理を行う
// 結果:NAGOYA FUKUOKA HOKKAIDO OKINAWA
}
}
Stream APIの処理は以下で構成されます.
①Data Source準備する→②Stream生成→③抽出/加工などの中間処理→④出力/集計などの終端処理
上の例でいえば,
①まずArrayListのData Sourceを作る
②Streamを生成する.ここではArrayList<String>をもとにしているため,
Streamメソッドも,Stream<String>オブジェクトを返します.
※与えられるData Sourceの型によって型引数は変動することと,
Streamの途中で値が加工されることで,型が変化していく場合もある.
③filterメソッドで「文字数が5より大きい値だけを取り出す」
mapメソッドで「大文字に変換する」
中間処理は複数あってもよし.省略してもよし.
④forEachメソッドで,得られた値をSystem.out::printlnメソッドで出力
終端処理は省略できません.
中間処理の戻り値はいずれもStream<T>です.
Stream APIでは,Streamの生成から中間処理/終端処理までを「.」演算子で
ひとまとめに連結でき,スマートな記述ができます.
(メソッドの連鎖という意味でメソッドチェーンと呼ばれる)
Streamの一連の処理が実行されるのは,終端処理のタイミングです.
中間で抽出/加工などの演算が呼び出されていても,それは一旦ストックされて,
その場では実行されず,終端処理まで処理の実施を待ちます.これを遅延処理といいます.
#Stream の作り方
##Collection/配列から生成
Collectionから
Collection.stream()
配列から
Arrays.stream(T[])
Mapから
Map.entrySet().stream()
stream()メソッドの並列版として,parallelStream()メソッドもあります.
streamをparallelStreamに置換するだけで,並列処理が可能になります.(強い…)
扱う要素数が多い場合,並列処理を有効にすることで,効率的に処理できる場合があります.
(勿論並列化のオーバーヘッドがあるため,必ずしも高速にはなりません.)
既存のStreamを並列化,あるいは直列化することもできます.
##StreamクラスからStream生成
Streamクラスでは,Stream生成するためのFactory Methodがあります.
最も基本的なのは,指定された可変数引数をStreamに変換するofメソッドです.
var stream = Stream.of("tokyo","osaka","nagoya");
stream.forEach(System.out::println); // tokyo, osaka, nagoya
他にもgenerate(),builder(),concat(),iterator()があります.ここでは割愛します.
##プリミティブ型のStream生成
IntStream
intで特殊化されたストリーム
LongStream
longで特殊化されたストリーム
DoubleStream
doubleで特殊化されたストリーム
IntStream.range(int start, int endExclusive) [第2引数は範囲外:開空間]
IntStream.rangeClosed(int start,int endInclusive) [第2引数は範囲内:閉空間]
IntStreamを使った繰り返し処理の例は以下のようになります.
for文を使った場合と比較します.ちょっとオサレですね.
for(int i = 1; i <= 5 ; i++){
System.out.println(i);
}
IntStream.rangeClosed(1, 5).forEach(System.out::println);
Javaのジェネリクスの型引数では,プリミティブ型は使えないため,
Stream<int>のような書き方はエラーになります.
##中間処理
Streamに流れる値を抽出/加工する役割を持ちます.
中間処理が実行されるのは,あくまで終端処理が呼び出されたタイミングであり,
呼び出しのたびに実行されるわけではないです.
###filter
指定された条件で値を抽出します.
Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).forEach(System.out::println); //tokyo
###map
与えられた値を加工します.
Stream.of("tokyo", "nagoya", "osaka").map(s -> s.length)).forEach(System.out::println); //5, 6, 5
生成直後はStream<String>だったのが,mapメソッドの後はStream<Integer>になってることに注意.
###sorted
要素を並べ替えます.
Stream.of("tokyo", "nagoya", "osaka").sorted().forEach(System.out::println); // nagoya, osaka, tokyo
Stream.of(2,1,3).sorted().forEach(System.out::println); // 1, 2, 3
デフォルトの動作は自然順序によるソートです.文字列ではれば辞書順,数値であれば大小でのソートです.
独自のソート規則を指定したい場合は,ソート規則をラムダ式で設定しましょう.
sorted()の引数はComparatorインターフェイスです.
Stream.of("tokyo", "nagoya", "osaka").
sorted((str1, str2) -> str1.length() - str2.length()).forEach(System.out::println); // tokyo, osaka, nagoya
###skip/limit
skip: m番目までの要素を切り捨てる
limit: n+1番目以降の要素を切り捨てる
IntStream.range(1, 10).skip(3).limit(5).forEach(System.out::println); // 4, 5, 6, 7, 8
skipメソッドで最初の4要素をスキップし,
limitメソッドでそこから5個分の要素を取り出しています.
limitメソッドではすでに先頭が切り捨てたStreamを操作するため,引数に注意しましょう.
###peek
Streamの途中状態を確認します.
peekメソッドそのものはStreamに影響を与えないため,主にデバッグ用に使われます.
Stream.of("tokyo", "nagoya", "osaka").peek(System.out::println).sorted().forEach(System.out::println);
//ソート前の結果:tokyo, nagoya, osaka ← peekのprintln
//ソート後の結果:nagoya, osaka, tokyo ← forEachのprintln
###distinct
値の重複を除去します.
Stream.of("tokyo", "nagoya", "osaka", "osaka", "nagoya", "tokyo").distinct().forEach(System.out::println);
// tokyo, nagoya, osaka
##終端処理
Streamに流れる値を最終的に出力/集計する役割を持ちます.
Streamは終端処理の呼び出しをトリガーにして最終的にまとめて処理されるため,
中間処理と異なり,終端処理は省略できません.
終端処理したStreamを再利用することはできないため,
再度Stream処理を行いたい場合は,StreamそのものをData Sourceから再生成する必要があります.
###forEach
個々の要素を順に処理します.
Stream.of("tokyo", "nagoya", "osaka").forEach(v -> System.out.println(v)); // tokyo, nagoya, osaka
Stream.of("tokyo", "nagoya", "osaka").forEach(System.out::println); // tokyo, nagoya, osaka
###findFirst
最初の値を取得します.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).findFirst().orElse("empty"));
// tokyo
空Streamの場合があるため,findFirstメソッドの戻り値はOptional型です.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("a")).findFirst().orElse("empty"));
// empty
###anyMatch/allMatch/noneMatch
値が特定の条件を満たすか判定します.
順に「条件式がtrueになる要素が存在するか」,「条件式がすべてtrueになるか」,
「条件式がすべてtrueにならないか」になります.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").anyMatch(v -> v.length() == 5)); // true
System.out.println(Stream.of("tokyo", "nagoya", "osaka").allMatch(v -> v.length() == 5)); // false
System.out.println(Stream.of("tokyo", "nagoya", "osaka").noneMatch(v -> v.length() == 5)); // false
###toArray
Stream処理の結果を文字列配列として変換します.
var list = Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).toArray();
###collect(コレクション変換)
Stream処理の結果をCollectionとして変換します.
collectメソッドにはCollectorsクラスで提供されている変換メソッドを渡します.
Listへの変換はtoList,Setへの変換はtoSet,マップへの変換はtoMapを使います.
var list = Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).collect(Collectors.toList());
collectメソッドはコレクション変換専用のメソッドというより,
リダクション処理を行うメソッドでもあります.リダクション処理の場合は後述します.
###min/max
最小値/最大値をもとめます.引数には比較規則(Comparator)を指定する必要があります.
戻り値がOptional型であるため,orElse経由になります.(ここではないことを意味する-1としています.)
System.out.println(Stream.of(1, 3, 2).min((int1, int2) -> int1 - int2).orElse(-1)); // 1
System.out.println(Stream.of(1, 3, 2).min((int1, int2) -> int2 - int1).orElse(-1)); // 3
System.out.println(Stream.of(8, 7, 9).max((int1, int2) -> int1 - int2).orElse(-1)); // 9
System.out.println(Stream.of(8, 7, 9).max((int1, int2) -> int2 - int1).orElse(-1)); // 7
###count
要素の個数を求めます.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.length() > 5).count()); // 1
###reduce
Streamの値を一つにまとめます(Reduction).
reduceメソッドは3種類のオーバーロードを提供しています.
####引数1個の場合
Optional reduce(BinaryOperator accumulator);
戻り値がOptional型であるため,orElse経由になります.
引数は演算結果を格納するための変数result,個々の要素を受け取るための変数strがあります.
System.out.println(
Stream.of("tokyo", "nagoya", "osaka").sorted()
.reduce((result, str) -> { return result + "," + str;}).orElse("")); // nagoya,osaka,tokyo
####引数2個の場合
T reduce(T identity, BinaryOperator accumulator);
第一引数で初期値を受け取ることができます.
結果は非nullであることが明らかなため,非Optional型になります.OrElse経由は不要です.
System.out.println(
Stream.of("tokyo", "nagoya", "osaka").sorted()
.reduce("hokkaido", (result, str) -> { return result + "," + str;})); //hokkaido,nagoya,osaka,tokyo
####引数3個の場合
U reduce(U identity, BiFunction accumulator, BinaryOperator combiner);
少々難しいかもしれません.Streamの要素型と,最終的な要素型が異なる場合に使います.ここでは例を割愛します.
詳細を知りたい方はこちらの記事を見るといいかもしれません.
###collect(リダクション操作)
Stream内の要素をCollectionなどにまとめます.
reduceがStream内の要素をint,Stringのような単一値にリダクション処理するのに対し,
collectはCollection/StringBuilderのような可変な入れ物に対して値を蓄積してから返します(可変リダクション).
こちらは少々難解なため,後程更新します….
#終わりに
自分は3月までにJava Gold SE 11を取得するつもりです.
(そもそも前提としてJava Silver SE 11を受けないといけませんが….)
Goldではラムダ式/Stream APIの出題率がとても高いため,整理しました.
Java SE 11 は半年前(2019/06下旬)くらいにできたばかりの資格で,
今まではSE 8 が最新版でした.Silver SE 11の参考書はすでに出ていますが,
Gold SE 11は現時点(2019/12)まだ参考書は出てないです.
SE 11 の試験ではSE 8の試験と違いラムダ式/Stream APIの使い方は
SE 11ベースになると思いますので,受験する方はご注意を.