Help us understand the problem. What is going on with this article?

[Java] Stream API 入門

Stream API #とは

こちらの記事の続きみたいなものです.

Stream API はDataをPipeLine形式で処理するためのAPIです.
Collection,配列,ファイルなどデータの集合体(Data Source)から,個々の要素を取り出して
これを「処理の流れ」(Stream)に引き渡すための仕組みを提供します.

Streamに対して関数操作を行った結果をStreamで返す「中間操作」と
処理結果をDataとして返す「終端操作」があります.

中間操作も終端操作もメソッド引数は関数型インターフェースを取るものが多いため,
ここでラムダ式の知識が利用していくとスマートということになります.
Stream API.png

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文を使った繰り返し
for(int i = 1; i <= 5 ; i++){
   System.out.println(i);
}
IntStreamを使った繰り返し
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があります.

引数1個の場合
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経由は不要です.

引数2個の場合
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ベースになると思いますので,受験する方はご注意を.

xrdnk
基本はブログに投稿してます.
https://xrdnk.hateblo.jp/
tis
創業40年超のSIerです。
https://www.tis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした