Processing math: 100%

Stream: ループを使わない

背景

不吉な匂い(Code Smell)という,バグではないものの,バグの温床となり得る場所を指す言葉があります. リファクタリング 第二版 という本で,不吉な匂いにループが新たに追加されています. forwhileなどのループはバグの温床になり得るので避けるべきと言われているのです. なぜなら,1行に書くべきことは1つと言われながらも,for文は1行に初期化式,継続条件,反復式の3つの式が存在しています. また,配列などの長さを超えてのアクセスは,インデックスで要素にアクセスしているために起こります.

Java 8 から Stream API と呼ばれるデータ処理を行うためのAPIが導入されており,これにより for などのループを置き換えることが可能になります. この Stream API とメソッド定義を簡略化して書けるラムダ式を用いることにより,より簡潔に分かりやすく書けるようになります. そして,この Stream API は Java だけでなく,昨今のプログラム言語で数多くサポートされています.

Stream API

第0講 基礎文法 例題で示した 1以上100未満の奇数一覧を出力するプログラムを例に挙げます.

元のプログラムのは次の通りです.

for(Integer i = 1; i < 100; i++){
  // iを割った余りが1であれば,奇数.
  if(i % 2 == 1){ // i % 2 != 0 の条件でも可.
    System.out.print(i);
    System.out.print(" ");
  }
}

上のプログラムを Stream API で書き直したものが下のものです. メソッドの返り値を変数に代入せず,そのままメソッド呼び出しを続けている点に注意して読んでください(メソッドチェイニング (Method chaining)) と呼びます). Terminal で jshell を実行し,以下のプログラムをコピー&ペーストして実行してみましょう.

System.out.println(                     // 得られた文字列を出力する.
  IntStream.range(1, 100)               // 1以上,100未満の各値に対して,続く処理を適用する.
    .filter(i -> i % 2 == 1)            // iを2で割った余りが1のもの(奇数)のみ残す.
    .mapToObj(i -> Integer.toString(i)) // Integer 型を String 型に変換する.
    .collect(Collectors.joining(" "))); // 全ての文字列を空白区切りで連結させる.

Stream APIは上記のように,繰り返しの中での様々な処理を関数を渡すことにより,副作用の少ないプログラムを書くことを目指します. 元のプログラムに比べての下のプログラムのメリットは次の通りです.

Streamの利用方法

Stream の生成法

Stream でできること

filter

Stream の要素を削除します. 引数(Predicate)は要素を受け取り Boolean を返します. 返り値がfalseのものはこのStreamから削除されます(元のListや配列から削除されるわけではありません).

map

Streamの各要素に対して,値の変換を適用します. 引数(Function)は,値を受け取り,変換後の値を返します(元のListや配列の要素が変換されるわけではありません).

forEach

Streamの各要素に対して,処理を行います. 引数(Consumer)は,値を受け取り,何も返しません.

collect

Map 型の Stream

Map はキーと対応するバリューのペアを格納します. そのため,そのまま Stream の実体を取得できません. 次のように,3つの方法で取得します(Map<K, V>型の変数mapから取得します.KVStringIntegerなどと読み替えてください)

forEach のみが必要であれば,mapに対して直接 forEach を呼び出すことも可能です. この場合,map.forEach((k, v) -> ...) のように,キーとバリューの両方を受け取ります.

Streamの適用例

奇数の一覧の別解

第0講 基礎文法 例題で示した 1以上100未満の 奇数一覧の別解.

System.out.println(
    IntStream.iterate(1, prev -> prev + 2) // 無限 stream を取得する.
        .takeWhile(value -> value <= 100)  // takeWhile は Java 11 から利用できる.
        // .limit(50)                      // 最初から50個の要素のみ取得する.Java 8だと takeWhile が使えないため.
        .mapToObj(value -> Integer.toString(value))
        .collect(Collectors.joining(" ")));

総和を求める.

第0講 練習問題3を Stream で解くと次のようなプログラムになります.

Integer sum(Integer from, Integer max) {
    return IntStream.rangeClosed(from, max) // from以上,max以下の範囲の各値に対して
      .sum();                               // 全てを合計する.
}

ArgsPrinter

第1講 例題 ArgsPrinter を Stream で解くと次のようなプログラムになります. argsのインデックス番号とその値を出力したいが,Arrays.stream を用いるとインデックス番号が取得できない. そのため,IntStream.range を用いる.

IntStream.rangeClosed(0, args.length)
    .forEach(i -> System.out.printf("%d: %s%n", i, args[i]));

モンテカルロ法によるπの計算

第2講 練習問題4

double pi(int loopCount) {
    return 4d * IntStream.range(0, loopCount) // 0以上,loopCount以下繰り返す.
        .map(i -> calculateDistance()) // 乱数で得た座標と原点の距離を計算する.
        .filter(length -> length < 1d) // 得られた距離が 1 より小さいもののみにする.
        .count();                      // 数を数える.
}

double calculateDistance() {
    double x = Math.random();
    double y = Math.random();
    return Math.sqrt(x * x + y * y);
}

参考資料