「Stream.generateを使ったら、プログラムが固まって動かなくなった」という経験はありませんか。実はこれ、1Z0-826の受験者が高確率で踏む罠です。無限ストリームの仕組みを正しく理解していないと、本番の試験でも「このコードの出力は何か」という設問で誤答してしまいます。本記事では、Stream.generateの基本動作から、無限ストリームを安全に扱うためのルール、試験で狙われるひっかけポイントまでを整理して解説します。読み終えたときには、無限ストリームに関する設問に自信を持って答えられるようになっているはずです。

Stream.generateとは何か

Stream.generateは、Supplier<T>を受け取り、要素を無限に生成し続けるストリームを作るメソッドです。シグネチャは次のようになっています。

static <T> Stream<T> generate(Supplier<T> s)

Supplierは「引数を取らず、値を返す」関数型インターフェースです。Stream.generateはこのSupplierを呼び出すたびに新しい要素を生成するため、ストリームの長さに上限がありません。

import java.util.stream.Stream;

public class GenerateDemo {
    public static void main(String[] args) {
        Stream.generate(() -> "Java")
              .limit(3)
              .forEach(System.out::println);
        // Java
        // Java
        // Java
    }
}

このように、limit()で要素数を制限しない限り、ストリームは終わりません。これがStream.generateを扱う上での最重要ポイントです。

Stream.iterateとの違い

前回の記事で扱ったStream.iterateは、前の要素から次の要素を計算して生成する点が特徴でした。一方Stream.generateは、各要素が完全に独立しており、前の要素との関連性を持ちません。この違いは試験でも頻出のため、表で整理します。

比較項目Stream.generateStream.iterate(seed, op)Stream.iterate(seed, predicate, op)
引数Supplier<T>T seed, UnaryOperator<T>T seed, Predicate<T>, UnaryOperator<T>
要素間の関連なし(各要素が独立)前の要素から計算前の要素から計算
ストリームの長さ常に無限常に無限条件によって有限
limit()が必須か必須必須不要(predicateがfalseで自動終了)
典型的な用途乱数生成、定数生成等差数列・等比数列範囲を指定した数列生成

3引数版のStream.iterateはJava 9以降に追加された機能で、predicateが偽になった時点で自動的にストリームが終了します。Stream.generateにはこのような自動終了の仕組みがないため、必ずlimit()などで制御する必要がある、という点が出題者の好む「違い」になります。

無限ストリームを安全に扱う方法

無限ストリームをそのまま終端処理に渡すと、プログラムは永久に処理を続けてしまいます。これを防ぐには、ストリームのどこかで要素数を制限する操作を挟む必要があります。最も基本的な方法がlimit()です。

import java.util.stream.Stream;

public class LimitDemo {
    public static void main(String[] args) {
        long count = Stream.generate(() -> 1)
                            .limit(5)
                            .count();
        System.out.println(count);
        // 5
    }
}

limit()を呼んだ時点でストリームは有限になるため、それ以降のcount()やcollect()といった終端処理も正常に終了します。逆に、limit()を呼ばずに以下のようなコードを書くと、プログラムはハングします(実行しないよう注意してください)。

// 危険なコード例:limitがないため無限に実行され続ける
Stream.generate(() -> "危険").forEach(System.out::println);

短絡操作と非短絡操作の違いに注意する

試験では「limit()を使っていないのに無限ストリームの処理が正常に終了するコード」が出題されることがあります。これは、短絡操作(short-circuiting operation)を使っているケースです。短絡操作は、条件を満たした時点で処理を打ち切るため、limit()がなくても無限ストリームに対して安全に使えます。

操作無限ストリームに使えるか理由
findFirst()使える最初の1件が見つかった時点で終了する
findAny()使える条件を満たす要素が見つかった時点で終了する
anyMatch()使える条件がtrueになった時点で終了する
allMatch()注意が必要条件がfalseにならない限り終了しない可能性がある
forEach()使えない全要素を処理しようとするため終了しない
collect()使えない全要素を集めようとするため終了しない
count()使えない全要素を数えようとするため終了しない
import java.util.stream.Stream;

public class ShortCircuitDemo {
    public static void main(String[] args) {
        boolean result = Stream.generate(() -> 1)
                                .anyMatch(n -> n == 1);
        System.out.println(result);
        // true
        // limit()がなくてもanyMatchは短絡操作なので正常終了する
    }
}

ここがまさに出題者の好むひっかけです。「limit()がないから無限ループになる」と短絡的に判断すると誤答してしまうため、使われている終端処理が短絡操作かどうかを必ず確認する習慣をつけてください。

実践的な活用例:乱数生成とカウンター生成

Stream.generateは、乱数のような「前の値に依存しない値」を生成する場面で特に有効です。

import java.util.Random;
import java.util.stream.Stream;

public class RandomDemo {
    public static void main(String[] args) {
        Random random = new Random();
        Stream.generate(random::nextInt)
              .limit(3)
              .forEach(System.out::println);
        // 実行ごとに異なる整数値が3つ出力される
    }
}

また、AtomicIntegerを組み合わせることで、連番のような状態を持つ値を生成することも可能です。

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

public class CounterDemo {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);
        Stream.generate(counter::incrementAndGet)
              .limit(5)
              .forEach(System.out::println);
        // 1
        // 2
        // 3
        // 4
        // 5
    }
}

ただし、このように外部の状態(counter変数)に依存するSupplierは「ステートフル」と呼ばれ、後述する並列処理と組み合わせた際に注意が必要になります。

並列処理時の注意点

Stream.generateで生成されるストリームは「順序を持たない(unordered)」という特性があります。そのため、parallel()と組み合わせると、要素の出力順序が保証されません。特に、AtomicIntegerのようなステートフルなSupplierを使っている場合、並列実行によって複数のスレッドが同時にSupplierを呼び出すため、順序が崩れるだけでなく、想定外の値が生成される可能性もあります。

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

public class ParallelGenerateDemo {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);
        Stream.generate(counter::incrementAndGet)
              .limit(5)
              .parallel()
              .forEach(System.out::println);
        // 出力順序は保証されない
        // 例:3, 1, 5, 2, 4 のように実行ごとに変化する
    }
}

試験では「順序が保証されているか」を問う設問が多く出されます。Stream.generateは順序を保証しない点を、Stream.iterateとセットで覚えておくと安心です。

試験対策のまとめ

最後に、Stream.generateに関して試験で問われやすいポイントを整理します。

ひっかけポイント正しい理解
limit()を使わなくても動くと思い込む短絡操作を使わない限り無限ループになる
Stream.iterateと同じ感覚で使うgenerateは要素間に関連がなく、前の値を引き継がない
forEachやcollectをそのまま使うこれらは非短絡操作のため、limit()が必須
並列処理でも順序が保たれると思うgenerateは順序を保証しないため、parallelでは順序が崩れる
ステートフルなSupplierを安全だと思う並列実行時に競合や予期しない値が発生する可能性がある

Stream.generateは、Supplierという単純な仕組みでありながら、無限ストリームという特性ゆえに多くの落とし穴を持っています。limit()の有無、短絡操作かどうか、並列処理による順序の崩れという3つの視点を押さえておけば、本番の試験でも落ち着いて正答を導けるはずです。次回は、Stream APIの中でも特に出題頻度の高いCollectorsの活用パターンについて解説していきます。