- [Java11] Stream まとめ -Streamによるメリット-
- [Java11] Stream 使い方まとめ -基本編-
Stream とは
コレクションに対するマップ-リデュース変換など、要素のストリームに対する関数型の操作をサポートするクラスです。
引用元: 公式ドキュメント
まずは実際に見てみる
公式リファレンスより抜き出したものを一部改変
final int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
上の例では、widgets
から color
がRED
のものを抽出してweight
の値の合計を計算しています。
for文で書くと以下のようになります。
int sum = 0;
for (Widget w: widgets) {
if (w.getColor() == RED) {
sum += w.getWeight();
}
}
何ができるの?
冒頭で書いた通りStreamとは「コレクションに対するマップ-リデュース変換など、要素のストリームに対する関数型の操作をサポートするクラスです。」
- コレクションに対するマップ-リデュース変換
ざっくり言うと今までfor文でやっていたこと、です。
上の合計値の計算然り、最大値の取得や新しいリストの作成などができます。
- 関数型の操作をサポートする
Streamクラスのメソッドの多くが関数型インターフェースを引数に取ります。例えばfilter関数にはどのようにフィルターをかけるか、mapToInt関数にはどのように整数型に変換するかを引数に渡します。
Streamには新しいStreamを返すメソッドがたくさん用意されており、そこに処理を渡すことで柔軟な処理が可能になっています。
何が良いの?
メリットとしては以下の 2 つが大きいと思います
- 可読性の向上
- 並列処理によるパフォーマンスの改善
可読性の向上
Streanを使った書き方が通常のコードと比べて**「宣言的」**だからです。
上の例の特に合計(sum
)を求める部分に着目してください。
-
Streamの場合
sum()
を呼び出しているだけ。
データの集合に対して、**「合計値を求めてくれ」**と言っているだけです。 -
for文の場合
sum
の初期値を0とし各要素の値を加算している。最終的な値が合計値となる。
**「合計値の求める方法」**を実装しています。
for文の方はStreamの方と比べて**「命令的」と呼びます。
「宣言的」な書き方では、細かい実装の隠蔽とコードの量の短縮ができます。やることを宣言しているだけなのでコードから何をしたいかが理解しやすい**です。
逆に「命令的」な書き方は、やる内容を細かく記述するため処理が複雑になる程、理解が難しくなりバグの原因にもなります。
経験上、Streamを使う使わないによらず、1つのメソッドにおける命令的なコードは少ない方が、読みやすく安全なコードが書けます。
並列処理によるパフォーマンスの改善
こちらの恩恵は得られないケースの方が多いです。(経験上)
なぜなら、余程大量のデータを扱わないかぎりは直列で実行する方が速いためです。
幸いStreanの実行を並列処理に変えるにはparallel()
呼び出すだけで済みます。
闇雲に並列処理にするのではなく、データ量、順番が保持されないことによる問題を考えて利用することで、並列処理の恩恵を最大限受けることができます。
以下の記事が参考になります。
参考: ラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識 (3/3)
どうやって書くの?
Stream を使って処理を書くには3 ステップあります。
- Stream を作成する
- 中間操作
- 終端操作
例で示すと以下の通りです。
final int sum = widgets.stream() // Streamの作成
.filter(w -> w.getColor() == RED) // 中間操作
.mapToInt(w -> w.getWeight()) // 中間操作
.sum(); // 終端操作
stream を作成する
ListやSetなどのCollectionから作成することが多いと思います。widgets.stream()
がまさにそれです。その他、いくつかの値からStreamを作る方法、Stream.Builderを使う方法、などがあります。
中間操作
filter
mapToInt
が中間操作にあたります。各要素の値の変換や条件に基づいた要素の抽出などを行います。
Stream<T>
やIntStream
などのStreamを返します。Streamを返すためメソッドチェーンが可能です。
中間操作の処理はfileter
などを呼び出したタイミングではなく。終端操作実行時に初めて実行されます。
実際に中間操作の実行タイミングがわかる例
import java.util.*;
import java.util.stream.*;
import java.awt.*;
import static java.awt.Color.RED;
import static java.awt.Color.BLUE;
public class Main {
public static void main(String[] args) throws Exception {
List<Widget> widgets = List.of(new Widget(RED, 10), new Widget(BLUE, 20));
Stream<Widget> stream1 = widgets.stream();
Stream<Widget> stream2 = stream1.filter(w -> w.getColor() == RED);
System.out.println("complete filtering");
IntStream stream3 = stream2.mapToInt(w -> w.getWeight());
System.out.println("complete mappint to integer");
final int sum = stream3.sum();
}
}
class Widget {
private Color color;
private int weight;
public Widget(Color color, int weight) {
this.color = color;
this.weight = weight;
}
public Color getColor() {
System.out.println(color);
return color;
}
public int getWeight() {
System.out.println(weight);
return weight;
}
}
complete filtering
complete mappint to integer
java.awt.Color[r=255,g=0,b=0]
10
java.awt.Color[r=0,g=0,b=255]
中間操作の直後には各処理が実行されていないことが見て取れます。
終端操作
sum
が終端操作にあたります。これ以外にはcollect
findFirst
などがあります。合計値や新しいコレクションを返したりします。
中間操作と違い返す値は様々です。できることは多種多様に及びます。「合計値を求める」「新しいCollectionの生成」「各要素への処理(forEach)」などがあります。
中間操作と終端操作のできることは実例を示した方が良いと思うので次回以降に詳しくまとめたいと思います。
関数型インターフェース
今まで無視してきましたが、w -> w.getColor() == RED
w -> w.getWeight()
の存在に触れたいと思います。
この文法をラムダ式と呼びますが、必ずしもラムダ式を書く必要はありません。
ラムダ式は関数型インターフェースを実装するための1つの手段にすぎません。
Streamのほとんどのメソッドが関数型インターフェースを引数にとるため、ここの理解はStreamの理解において避けては通れません。
正直、最初は雰囲気で書いてしまっても問題ないのでここから先は最悪理解できなくてもStreamを書くには問題ないかもしれないです。
関数型インターフェースとは
インターフェースの一種です。このインターフェースはたった1つのメソッドを持ちます
Javaに標準で用意されているもののほか、自分で作成することもできます。
それぞれの型がわかりやすいように、最初の例をなるべく分割すると以下のようになります。
Stream<Widget> stream1 = widgets.stream();
Predicate<Widget> predicate = w -> w.getColor() == RED;
Stream<Widget> stream2 = stream1.filter(predicate);
ToIntFunction<Widget> toIntFunction = w -> w.getWeight();
IntStream stream3 = stream2.mapToInt(toIntFunction);
final int sum = stream3.sum();
ラムダ式の代入先になっているPredicate
ToIntFunction
が関数型インターフェースです。ToIntFunction
の定義は以下のようになっています。
@FunctionalInterface
public interface ToIntFunction<T> {
int applyAsInt(T value);
}
関数型インターフェースには@FunctionalInterface
を付与しますが、付けなくとも機能的には問題ありません。
実装の方法
Streamのメソッドに渡すために関数型インターフェースを実装したインスタンスを生成する必要がありますが、実装の方法は主に3通りあります。
- 匿名クラス
- ラムダ式
- メソッド参照
です。
では、それぞれの実装方法を比較してみましょう。
前提として、このようなWidget
クラスがあるものとします。
class Widget {
private Color color;
private int weight;
public Widget(Color color, int weight) {
this.color = color;
this.weight = weight;
}
public Color getColor() {
return color;
}
public boolean isColorRed() {
return color == RED;
}
public int getWeight() {
return weight;
}
}
匿名クラス
とても読みやすいとは言えないです。
final int sum = widgets.stream()
.filter(new Predicate<Widget>() {
public boolean test(Widget w) {
return w.isColorRed();
}
})
.mapToInt(new ToIntFunction<Widget>() {
public int applyAsInt(Widget w) {
return w.getWeight();
}
})
.sum();
一応これでもできます...
当然これでも動きます。
static class WidgetTestColorIsRed implements Predicate<Widget> {
public boolean test(Widget w) {
return w.isColorRed();
}
}
static class WidgetToWeightFunction implements ToIntFunction<Widget> {
public int applyAsInt(Widget w) {
return w.getWeight();
}
}
final int sum = widgets.stream()
.filter(new WidgetTestColorIsRed())
.mapToInt(new WidgetToWeightFunction())
.sum();
まぁ、書けるだけで書くケースは全くと言っていいほどないと思います。
ラムダ式
圧倒的に読みやすくなりました。
final int sum = widgets.stream()
.filter(w -> w.isColorRed())
.mapToInt(w -> w.getWeight())
.sum();
ラムダ式の書き方にも何通りかあります。
// 引数なし
() -> "定数";
// 引数1個
n -> n + 1; // 括弧の省略が可能
(n) -> 2 * n;
// 引数2個以上
(a, b) -> Math.sqrt(a * a + b * b);
(x, y, z) -> x * y * z;
// 複数行
(a, b, c) -> {
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
メソッド参照
ここまで使ってきませんでしたが、メソッド参照で書ける場合は積極的に使った方がコードが見やすくなります。
w
のような変数を置かないで済むのと、その時点でStreamの要素の型が何なのかが分かるので読みやすいです。
クラス::メソッド
のように書きます。
final int sum = widgets.stream()
.filter(Widget::isColorRed)
.mapToInt(Widget::getWeight)
.sum();
匿名クラスもラムダ式もメソッド参照も関数型インターフェースのインスタンスを生成するための方法です。自分で定義した関数型インターフェースであっても同様の方法でインスタンスの作成が可能です。
関数型インターフェースでは、たった1つのメソッドを実装すればいいことがわかっているため、ラムダ式のような型の情報が全くない書き方でも推論でどうにかなるわけです。
おわりに
次回、Streamの具体的な使い方を紹介したいと思います。