Java Stream APIをいまさら入門

  • 44
    Like
  • 1
    Comment

Stream API?

Stream APIとはJava SE 8から追加されたイテレーションの拡張APIです。
今までコレクションに対して行っていた煩雑な処理をわかりやすいコードで記述することが可能になります。
(本稿ではラムダ式の解説は行いません。ラムダ式がわからない人は調べてください。)

streamとはコレクションの要素へのアクセスを許可されたもの、コレクションの別の形態と考えてください。

基本的な流れ
1. コレクションからstreamを取得
2. streamに対して満足するまで「中間操作」を実行。コレクションの中身を都合よく変換
3. 「終端操作」で変換したコレクションの中身に対して処理を適用する

では実際の使用例を見てみましょう。

Stream APIの使い方

サンプル「1〜5の中で偶数だけを表示する」

  • Stream APIを使わない場合
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : integerList) {
    if (i % 2 == 0) {
        System.out.println(i);
    }
}
  • StreamAPIを使う場合
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // streamの取得
        .filter(i -> i % 2 == 0) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

処理の違いを見てみましょう。
Stream APIを使わないパターンでは「2で割り切れたら表示」という構造になっています。
Stream APIを使うパターンでは「2で割り切れるものを集める」→「要素をすべて出力」という構造になっています。

2処理に分割できています。可読性が上がるほか、部品性やコードの抽象度も高まっています。

今回のサンプルではstream取得後、中間操作として「filter」メソッドを使っています。
名前の通り、コレクションの要素をフィルタリングするものです。
filterメソッドには T -> boolean となるラムダ式を渡してあげます。
(※ Tの意味がわからない人はジェネリクスで調べてください。)
ラムダ式がtrueの要素だけが残ったstreamを返します。

終端処理として「forEach」メソッドを使っています。
これも名前も通り「要素を1つずつ取り出していって処理を実行」するメソッドです。
他言語ではお馴染みの構文ですね。Javaだと拡張for文に相当します。

サンプルでは「2で割ったらあまりが0になるものだけ」(中間操作)を「1つずつ画面に出力する」(終端処理)という意味になります。

中間操作の返り値には操作適用後のstreamが返ってくるのでサンプルのように連続して書くことができます。
もちろん、次のように書くことも可能です。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
        Stream<Integer> stream = integerList.stream(); // streamの取得
        Stream<Integer> stream2 = stream.filter(i -> i % 2 == 0); // 中間操作
        stream2.forEach(i -> System.out.println(i)); // 終端操作

つなげて書いたほうが可読性があがるので特別な理由がない限り、連結して記述しましょう。

様々な中間操作

よく使う中間操作を挙げていきます。

filter

先ほどのサンプルで挙げたようにフィルタリングをするための中間操作です。
引数には T -> boolean となるラムダ式を渡してあげます。
式がtrueの要素だけを集めます。

偶数取り出し
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // streamの取得
        .filter(i -> i % 2 == 0) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

map

map の引数には T -> U となるラムダ式を渡してあげます。
要素を変換する中間操作です。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // streamの取得
        .map(i -> i * 2) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

このように2倍にしてみたり、

要素メッセージを表示
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // streamの取得
        .map(i -> "要素は" + i + "です") // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

このように別の型を返すように(この例ではIntegerからString)にしたりすることもできます。
また、次のような動物インターフェースがあるとします。

Animalインターフェース
interface Animal {
    String getCry();    // 鳴き声を取得
}

動物のリストに対してmapを使い、鳴き声を出力してみましょう。

mapのサンプル
List<Animal> animalList = Arrays.asList(dog, cat, elephant);
animalList.stream() // streamの取得
        .map(animal -> animal.getCry()) // 中間操作
        .forEach(cry -> System.out.println(cry)); // 終端操作

sorted

map の引数には (T, T) -> int となるラムダ式を渡してあげます。
要素を並び替える中間操作です。
要素を2つずつ取り、ラムダ式に渡していきます。返り値が正なら降順、負なら昇順になります。

降順のサンプル1
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // streamの取得
        .sorted((a, b) -> b - a) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

しかし、単純なソート(昇順、降順)である場合はComparatorインターフェースのnaturalOrder()、reverseOrder()を渡してやるのが一番わかり易いと懐います。

降順のサンプル2
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // streamの取得
        .sorted(Comparator.reverseOrder()) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

以上3つが頻繁に使う中間操作です。
他にも色々あるので興味がある方は調べてみてください。

終端操作

forEach

forEach の引数には (T) -> void となるラムダ式を渡してあげます。
要素を1つずつ取り出し何らかの処理をする終端操作です。
使い方は今までのサンプルを見てください。