Concepts / Creating Streams from Collections and Arrays

Creating Streams from Collections and Arrays

Stream.generate(supplier) creates a sequential stream that is effectively infinite: it keeps producing values forever or until producing one causes an error.

  • Programming

Streams That Keep Going

Most of the streams you have met so far have a clear beginning and a clear end. A stream built from a list stops when the list runs out of elements. This article looks at a different kind of stream, one that does not run out. Its values keep arriving for as long as the pipeline keeps asking for them.

As you read, keep two questions in mind: what is the source of the values, and what would tell the program to stop? The answers to both questions decide what the program prints and whether it finishes at all.

java
Notes on the code
  • Stream.generate takes a supplier; here the supplier is the lambda () -> counter[0]++
  • counter is a one-element array, so the lambda can change its contents while the reference stays fixed
  • limit(5) is the stopping point that bounds the infinite stream
  • forEach is the terminal operation that asks the pipeline for values

What do you think happens?

What does this program print, and how many lines does it produce?

Reveal answer
Answer:

It prints 0, 1, 2, 3, and 4 on five separate lines, then the program finishes.

The post-increment returns the old value before adding one, so the first call returns 0 and the counter then holds 1. limit(5) allows exactly five supplier requests, so exactly five values are printed.

Tracing the Supplier Calls

The program above is built around one idea: the supplier does not run on its own schedule. It runs only when the terminal operation asks for a value. The diagram below follows the requests one at a time. Each request causes exactly one supplier call, and each call returns the next value in the sequence.

Request 1supplier returns 0Request 2supplier returns 1Request 3supplier returns 2Request 4supplier returns 3Request 5supplier returns 4limit reachedno further requestsnextnextnextnextcutoff
When Stream.generate(supplier) is chained with limit(5), in what order do the supplier calls and limit's cutoff happen, and why does the stream stop after five elements instead of running forever?

Read the diagram from left to right. limit does not generate values itself. It counts the requests coming through the pipeline, and once five have passed, it stops sending requests upstream. The supplier only ever runs in response to a request, so the five-request cutoff is what makes the program finish.

What Makes a Stream Effectively Infinite

Definition

Stream.generate(supplier) creates a sequential stream that is effectively infinite. The stream will continue to produce values forever, or until trying to do so produces an error. Nothing in the stream itself marks an end.

Each time the pipeline needs another element, the stream asks the supplier for one more value. The supplier is the only thing that determines what those values are. If the supplier returns the same value every time, the stream carries that value forever. If the supplier changes a value on each call, as the counter in the example does, the stream carries a changing sequence.

Because the stream is sequential, its elements are produced and consumed one at a time in encounter order. This is what makes the counter behave predictably: each supplier call happens in turn, so the values appear in the same order every time the program runs.

Three Forms of generate

The same idea appears in three forms. Stream.generate takes a supplier and produces a stream of whatever type that supplier returns. The two primitive variants are specialized for numbers, and they take their own supplier interfaces. The table summarizes the three methods.

MethodSupplier it acceptsStream it produces
Stream.generate(s)a Suppliera Stream of whatever type the supplier returns
IntStream.generate(s)an IntSuppliera stream of int values
DoubleStream.generate(s)a DoubleSuppliera stream of double values

The three generate methods and the functional interface each one accepts

The primitive versions avoid boxing the values into objects. That is the reason to choose IntStream.generate over Stream.generate when you are producing numbers. The code below shows the primitive form with the same counter pattern.

java
Output
0
1
2
Notes on the code
  • IntStream.generate accepts an IntSupplier, so the lambda returns an int
  • limit(3) bounds the stream after three values
  • Output shows three lines because three values were requested

Finding Where the Output Goes Wrong

When an infinite stream prints something unexpected, compare the printed values with your prediction in order. The first position where they differ identifies the supplier call that behaved differently. Because the stream is sequential, that first wrong value is the earliest point of failure, and every later value may simply be following from it.

Next, check the places where state can change: the supplier's body and any variable it reads. In the counter example, the post-increment inside the supplier is the only place the counter moves. Changing the operator changes the whole sequence, as the comparison below shows.

Before
int[] counter = {0};
Stream.generate(() -> counter[0]++)
      .limit(5)
      .forEach(System.out::println);

Post-increment returns the old value, so the first printed value is 0

After
int[] counter = {0};
Stream.generate(() -> ++counter[0])
      .limit(5)
      .forEach(System.out::println);

Pre-increment returns the new value, so the first printed value is 1 and the sequence shifts by one

In the after version, the first printed line is 1 instead of 0. The divergence appears at the first printed line, not at the increment itself, which is why comparing outputs in order is the right first step.

  • Common mistake

    Expecting the stream to end on its own

    What it looks like
    Stream.generate(() -> counter[0]++).forEach(System.out::println);
    Why it fails

    A generated stream has no built-in end, so the requests never stop.

    Do this instead

    Add limit(n) before the terminal operation to bound the stream.

  • Common mistake

    Looking for the bug in the wrong line

    What it looks like

    Checking the forEach call when the printed values are already off by one

    Why it fails

    The stream is sequential, so the first wrong value is the earliest point of failure.

    Do this instead

    Find the first differing value in order, then inspect the supplier's body.

  • Common mistake

    Assuming the primitive variant needs a different kind of supplier logic

    Why it fails

    IntStream.generate uses an IntSupplier and DoubleStream.generate uses a DoubleSupplier; the counter pattern is the same.

    Do this instead

    Keep the same reasoning and check which supplier interface the method accepts.

Creating Streams from a Collection

A stream is a way of processing a sequence of values, but it needs a starting point that supplies those values. In this article the starting point is a collection such as a list or a set. Lists and sets both serve as stream sources in the same way, so you do not need a different technique for each type of collection. What changes between creation calls is the kind of stream that comes out, not the kind of collection.

Given a collection named c, calling c.stream() produces a sequential stream holding the same values as the collection. Calling c.parallelStream() produces a parallel stream from the same collection. A second route also reaches a parallel stream: you create a sequential stream with stream() and then call parallel() on it. The result is a parallel stream, so this route reaches the same destination as parallelStream() with one extra step.

java
Output
false
true
true
Notes on the code
  • stream() gives a sequential stream, so isParallel() reports false
  • parallelStream() gives a parallel stream directly
  • stream() followed by parallel() also gives a parallel stream
c.stream()one callc.parallelStream()one callc.stream().parallel()two callsthen parallel()
Which of the three calls (collection.stream(), collection.parallelStream(), or an existing stream's parallel()) produces a sequential stream and which produces a parallel one, and how do they relate to each other?

The method name tells you which kind of stream you are asking for: stream() for sequential, parallelStream() for parallel. Keeping the names straight is the core skill here. The source also notes that a sequential stream produces its elements in encounter order, which is why sequential results are predictable from run to run.

Predicting the Kind of Stream

Worked example

Choosing the creation call for a parallel stream

A program holds a List<String> named words and needs a parallel stream. Three calls are proposed: words.stream(), words.parallelStream(), and words.stream().parallel(). Which of them produce a parallel stream?

  1. 1
    Read the first call

    words.stream() calls the method whose name indicates a sequential stream, so it is not parallel.

  2. 2
    Read the second call

    words.parallelStream() calls the method whose name indicates a parallel stream directly, so it is parallel.

  3. 3
    Read the third call

    words.stream().parallel() starts sequential and then calls parallel(), which yields a parallel stream.

  4. 4
    Check against the expected result

    Two of the three calls produce parallel streams, and the first is the only sequential one.

Result

words.stream() is sequential. words.parallelStream() and words.stream().parallel() both produce parallel streams.

Practice: Predicting and Verifying

EASY

A program runs Stream.generate(() -> ++counter[0]).limit(4).forEach(System.out::println) with counter initialized to {0}. Write down the four printed values before checking. Then identify the first printed line where your prediction could differ, and explain which part of the supplier causes that difference.

Hints
  • Pre-increment returns the new value, so consider what the first call returns.
  • Compare your four predicted values with the printed values in order, starting from the first line.
  • The supplier's body is the only place the counter changes.

Key Takeaways

  • Stream.generate(supplier) creates a sequential, effectively infinite stream whose values come only from the supplier, so a stopping point such as limit() is required before a terminal operation.
  • The supplier runs only when the pipeline requests a value, so limit() decides how many supplier calls happen.
  • Stream.generate, IntStream.generate, and DoubleStream.generate accept a Supplier, IntSupplier, and DoubleSupplier respectively; the primitive versions avoid boxing.
  • To find an output divergence, compare printed values in order, find the first difference, and then inspect the supplier.
  • collection.stream() and collection.parallelStream() create sequential and parallel streams from the same collection, and stream().parallel() reaches a parallel stream in two steps.