[初学者向け]Java8 Stream APIの使い方(前編)
本記事は「Java Advent Calendar 2023」の参加記事です。
こんにちは。システム開発本部の佐々木です。
今月はQiitaさんのアドベントカレンダーに参加するということでバリバリ記事書くぞ〜と意気込んでいたんですが、先々週コロナにかかりダウン、先週は胃腸炎のようなものにかかりダウン、と絶不調で滞っておりました。
皆さんも体調にはくれぐれもお気をつけて、元気に年を越しましょう。
今回はタイトルにもあるようにJava 8から導入されたStreamという機能について、初心者の方向けに使い方をまとめてみます。
英語の「stream」という言葉は「流れ」を意味していて、JavaにおけるStream APIはデータの集合を1つの「流れ」として抽象化し、流れてくる各データに対して施す処理を記述することを容易にしてくれます。
そんなStreamを扱う際の基本中の基本として2種類の操作があります。それが「中間操作」と「終端操作」です。
今回は前者の「中間操作」の中からよく使うものをいくつかピックアップしてご紹介します。(「終端操作」は次回でまとめます。)
中間操作とはStream化されて流れてくるデータに対してごにょごにょする操作(メソッド)を指します。
ごにょごにょって具体、何ができるんですか?!というのが早く知りたいと思いますので、さっそく具体的にメソッドを紹介していきます。
1. map
私の体感ですが最もよく使う中間操作の一つです。
データを受け取って、何かを返すというものです。(同一Streamとしては返す値の型は揃っていなくてはなりません。)
Stream.of("a", "b", "c")
.map(char -> char + char + char)
// "aaa", "bbb", "ccc" のStreamになる。
シンプルな使用例は上記です。流れてきたアルファベットを受け取って三重に加工して受け流す、そんな動作です。
他にも、データの集合のうち条件に当てはまるデータだけ加工することもできます。
Stream.of(1, 2, 3, 4, 5)
// 偶数だけ二乗する。
.map(num -> num % 2 == 0 ? num * num : num)
// 1, 4, 3, 16, 5 のStreamになる。
2. filter
これも最もよく使う中間操作の一つです。mapとfilterさえ押さえておけば割とどうとでもなります。
filterは文字通り、流れてくるデータを間引きしてくれます。
Stream.of(1, 2, 3, 4, 5)
// 偶数を間引いて奇数のみのStreamにする。
.filter(num -> num % 2 == 1)
// 1, 3, 5 のStreamになる
便利ですね。私はJavaを学び始めた当初からすでにStreamは存在していたので当たり前に感じますが、Java 8より前の時代からJava触っている方々からすると結構革命的だったんでしょうかね?
Stream.of(11, 12, 13, 14, 15)
// 二乗して200より大きくなるものを間引いて、それ以外は二乗する。
.filter(num -> num * num < 200)
.map(num -> num * num)
// 121, 144, 169, 196 のStreamになる
こんな感じでmapとfilterの合わせ技も使えるのでかなり出来ることの幅は広いです。
3. distinct
こちらはStreamとして扱うデータの集合において重複があり得る場合に役立つメソッドです。名前でなんとなく察しがつく方もいると思いますが、この子はStreamから重複を消してくれます。
引数には何も受け取りません。ただStreamの一連の操作の中にこれを挟むとそのタイミング以降は重複する値は間引かれてそのStream内での各データの値の一意性が生まれます。
Stream.of("佐々木", "佐藤", "佐々木", "田中", "佐藤", "佐藤")
.distinct()
// "佐々木", "佐藤", "田中" のStreamになる
4. peek
これは私自身結構あとになって存在を知った中間操作のメソッドなのですが、私の浅い経験上では実際にプロダクトコードとしてというよりはデバッグ時に使うことが多いです。
どういうメソッドなのかというと、流れてくる値を受け取って何も返さないのです。
値を受け取れはするんですが、peekの中でいくらその値を加工しようとも、後続には元のまま値は流れて行きます。
Stream.of(1, 2, 3, 4, 5)
.peek(num -> { num = num * num })
// 1, 2, 3, 4, 5 のStreamのまま
じゃあ何の意味があるんだというと、デバッグに使えます。
Streamを用いたコード周辺で意図しない挙動が起きている場合に、要所要所にpeekでログ出力するようにして実行させると、Streamの一連の処理のうち、このタイミングでは各データはこういう状態で…というのを把握することが容易にできます。
とは言ってもpeekメソッドが存在しなかったとしてもmapを使ったりしてカバーはできるので、あくまで知ってると少し楽な時もあるよーくらいのノリです。
Stream.of(1, 2, 3, 4, 5)
.filter(num -> num % 2 == 1)
.peek(num -> System.out.println(num)) // ここに流れてくる1, 3, 5がログ出力される
.map(num -> {
System.out.println(num);
return num;
}) // やってることは3行目のpeekと同じ
終わりに
ここまで4つの中間操作メソッドをご紹介しました。決して莫大な量ではないので他のメソッドやさらに具体的な挙動など知りたくなったらStreamの公式リファレンスを覗いてみるのが良いかと思います。
自分もこの記事を書く上で久々に読み返してみて非常に良い復習の機会になりました。
次回は終端操作の方のメソッドを紹介しようと思っております。乞うご期待!