forやifで書くアレをStream APIで書く

前回の続編です。

前回の記事↓
Stream APIにチャレンジ! - Java EE 事始め!

Java SE 8ローンチイベントでの@cero_tさんの発表の中で、新しい定石として
「forやwhileを見かけたらStream APIへの置き換えを考える」
というのがありました。今回はこれにチャレンジしようと思います。

1~20までの偶数のみの合計を求める(答は110)

Java 7までの書き方
int sum1 = 0;
for (int i = 1; i <= 20; i++) {
    if (i % 2 == 0) {
        sum1 += i;
    }
}
System.out.println(sum1);
Java 8での書き方①
int sum2 = IntStream.rangeClosed(1, 20)
        .filter(i -> i % 2 == 0)
        .sum();
System.out.println(sum2);

IntStream.rangeClosed(1, 20)で、1,2,3,…,19,20というIntStreamを生成し、filter(i -> i % 2 == 0)で偶数のみ抽出し、sum()で合計を求めます。

Java 8での書き方②
int sum3 = IntStream.rangeClosed(1, 10)
        .map(i -> i * 2)
        .sum();
System.out.println(sum3);

1~10までのIntStreamを生成し、map(i -> i * 2)で各値を2倍に変換します。

Java 8での書き方③
int sum4 = IntStream.rangeClosed(1, 10)
        .map(i -> i * 2)
        .reduce(0, (i1, i2) -> i1 + i2);
System.out.println(sum4);

和を求めるのにreduce()を使っています。
((((0 + 2) + 4) + 6) + 8)…+20)という計算をしています。
単純に合計を求めるだけならsum()でいいと思います。
後で出てくる階乗の計算などは、reduce()を使う必要があります。

1の階乗~10の階乗をすべて求める

Java 7までの書き方
for (int i = 1; i <= 10; i++) {
    int kaijo = 1;
    for (int j = 1; j <= i; j++) {
        kaijo *= j;
    }
    System.out.println(i + "! = " + kaijo);
}
Java 8での書き方①
IntStream.rangeClosed(1, 10)
        .forEach(i -> {
            int kaijo = IntStream.rangeClosed(1, i).reduce(1, (i1, i2) -> i1 * i2);
            System.out.println(i + "! = " + kaijo);
        });

reduce()で階乗を計算しています。
forEach()使っちゃってるので、マサカリが飛んできそうなコードです。

Java 8での書き方②
IntStream.rangeClosed(1, 10)
        .map(i -> IntStream.rangeClosed(1, i).reduce(1, (i1, i2) -> i1 * i2))
        .forEach(i -> System.out.println(i));

map()を利用して、Streamの各要素を階乗の値に変換しています。
ただ、これでは元の1,2,3,…というデータが消えてしまうので、「3! = 6」というような表示が出来ません。

Java 8での書き方③
IntStream.rangeClosed(1, 10)
        .boxed()  // .mapToObj(i -> i) でも同様
        .collect(Collectors.toMap(i -> i, 
                i -> IntStream.rangeClosed(1, i).reduce(1, (i1, i2) -> i1 * i2)))
        .forEach((k, v) -> System.out.println(k + "! = " + v));

キーが整数、値がキーの階乗というMapに変換しました。
boxed()は、IntStreamをStream<Integer>に変換する中間処理です。
これだと、
1! = 1
2! = 2

という表示が出来ます。
Collectors.toMap()は、2つのラムダ式を引数にとります。
2つとも、ラムダの引数はストリームの各要素になります。
1つ目のラムダの戻り値がMapのキー、2つ目のラムダの戻り値がMapの値になります。

FizzBuzz

Java 7までの書き方
for (int i = 1; i <= 40; i++) {
    if (i % 15 == 0) {
        System.out.print("FizzBuzz");
    } else if (i % 3 == 0) {
        System.out.print("Fizz");
    } else if (i % 5 == 0) {
        System.out.print("Buzz");
    } else {
        System.out.print(i);
    }
    System.out.print(",");
}

お馴染みのコードだと思います。

Java 8での書き方①
IntStream.rangeClosed(1, 40)
        .forEach(i -> {
            if (i % 15 == 0) {
                System.out.print("FizzBuzz");
            } else if (i % 3 == 0) {
                System.out.print("Fizz");
            } else if (i % 5 == 0) {
                System.out.print("Buzz");
            } else {
                System.out.print(i);
            }
            System.out.print(",");
        });

これはマサカリコードです。

Java 8での書き方②
IntStream.rangeClosed(1, 40)
        .mapToObj(i -> i % 15 == 0 ? "FizzBuzz" 
                : i % 3 == 0 ? "Fizz" 
                        : i % 5 == 0 ? "Buzz" 
                                : String.valueOf(i))
        .forEach(s -> System.out.print(s + ","));

これも、個人的には100%は納得していないのですが、今のところこれしか思いつきませんでした。
mapToObj()でIntStreamからStream<String>に変換します。
条件演算子をネストしています。

1~100までの素数をすべて求める

Java 7までの書き方
List<Integer> result1 = new LinkedList<>();
for (int i = 2; i <= 100; i++) {
    boolean isPrime = true;
    for (int j = 2; j <= Math.sqrt(i); j++) {
        if (i % j == 0) {
            isPrime = false;
            break;
        }
    }
    if (isPrime) {
        result1.add(i);
    }
}
result1.forEach(i -> System.out.print(i + ","));
Java 8での書き方
List<Integer> source = IntStream.rangeClosed(2, 100)
        .boxed()
        .collect(Collectors.toCollection(LinkedList::new));
List<Integer> result2 = new LinkedList<>();
while (true) {
    int firstElement = source.get(0);
    if (firstElement > Math.sqrt(100)) {
        break;
    }
    result2.add(firstElement);
    source.removeIf(elem -> elem % firstElement == 0);
}
result2.addAll(source);
result2.forEach(i -> System.out.print(i + ","));

「エラストテネスのふるい」というアルゴリズムを使ってみました。
エラトステネスの篩 - Wikipedia
ただし、まだwhile文が残っています。これをStream APIで書き換える方法がまだ思いついていません。
引き続き、宿題ということで…。