Java Stream generate
last modified May 25, 2025
This article demonstrates how to use the Java Stream generate method to create infinite streams of data.
Stream.generate is a static factory method that creates an infinite, unordered stream where each element is generated by the provided Supplier. This method is useful for creating streams of random data, constant values, or stateful generators.
The generate method is part of the Java Stream API, introduced in Java 8, and it allows for lazy evaluation of elements. This means that elements are only generated when they are needed, which can be efficient for large or infinite streams.
The method signature for Stream.generate
is:
static <T> Stream<T> generate(Supplier<? extends T> s)
The Supplier is a functional interface that provides elements of type T. The
resulting stream is infinite, so it typically needs to be limited using
operations like limit. The Supplier
function is called each time
an element is requested, allowing for dynamic generation of values.
Generating constant values
Creating a stream of constant values.
void main() { Stream.generate(() -> "falcon") .limit(5) .forEach(System.out::println); }
This example creates an infinite stream of "falcon" strings and limits it to 5
elements. The Supplier
lambda () -> "falcon"
provides the constant value.
$ java Main.java falcon falcon falcon falcon falcon
Generating random numbers
We create a stream of random numbers.
void main() { Random random = new Random(); Stream.generate(random::nextDouble) .limit(5) .forEach(System.out::println); }
This example generates random doubles between 0.0 and 1.0. The method reference
random::nextDouble
serves as the Supplier
for the
stream.
$ java Main.java 0.7310570414761102 0.2390504976470176 0.003338256382940902 0.9564232111672854 0.7865432763945762
Stateful generators
The generate method can also be used with stateful Suppliers, which maintain state between calls. This allows for generating sequences or dynamic values that depend on previous calls.
void main() { AtomicInteger counter = new AtomicInteger(0); Stream.generate(() -> "Item " + counter.getAndIncrement()) .limit(10) .forEach(System.out::println); }
This example demonstrates a stateful generator using AtomicInteger to create sequentially numbered items. Note that stateful Suppliers should be used with caution in parallel streams.
$ java Main.java Item 0 Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 Item 7 Item 8 Item 9
Fibonacci sequence
The generate method can be used to create sequences like the Fibonacci
sequence, where each element depends on the previous ones. This requires a
custom Supplier
that maintains state between calls.
class FibonacciSupplier { private long prev = 0; private long curr = 1; public long get() { long next = prev + curr; prev = curr; curr = next; return prev; } } void main() { var fibSupplier = new FibonacciSupplier(); Stream.generate(fibSupplier::get) .limit(10) .forEach(System.out::println); }
This example generates the Fibonacci sequence using a custom stateful Supplier class. The Supplier maintains state between calls to generate the sequence.
$ java Main.java 1 1 2 3 5 8 13 21 34 55
Using AtomicLong
The following example demonstrates how to use AtomicLong
with
Stream.generate
to create a sequence of numbers in a thread-safe
manner. The AtomicLong
class provides atomic operations to update a
long
value, which is particularly useful when managing state in
streams, especially in concurrent scenarios.
void main() { var val = new AtomicLong(2L); Listdata = Stream.generate(() -> val.getAndAdd(2)) .limit(20) .toList(); System.out.println(data); }
The code above generates a list of 20 even numbers starting from 2 (i.e., [2, 4,
6, ..., 40]) by using AtomicLong
to maintain state within the
stream. The getAndAdd
method atomically retrieves the current value
and increments it by 2 for each element.
When working with state in Java streams, such as in the
Stream.generate
method, variables used within lambda expressions
(like the supplier () -> val.getAndAdd(2)
) must be final or
effectively final. This restriction arises because lambda expressions in Java
capture variables by reference, and Java enforces strict rules to prevent
unintended side effects in concurrent or functional programming contexts.
Final or effectively final requirement means that a variable is final if it's
explicitly declared with the final
keyword, meaning it cannot be
reassigned after initialization. A variable is effectively final if it's not
declared final
but is never reassigned after its initial value is
set. Java requires variables used in lambda expressions (or anonymous classes)
to be final or effectively final to ensure predictable behavior, especially in
parallel streams where multiple threads might access the same variable.
In the example, the variable val
is declared as var val = new AtomicLong(2L);
.
It is effectively final because it is not reassigned to
a new AtomicLong
object after initialization. The lambda expression
() -> val.getAndAdd(2)
captures val
and uses it to
generate stream elements. If you attempted to reassign val
(e.g.,
val = new AtomicLong(10L);
) later in the method, the lambda would
fail to compile with an error like: "Variable used in lambda expression
should be final or effectively final." This rule prevents accidental
modifications that could lead to unpredictable results in stream processing.
While the val
reference itself is effectively final,
AtomicLong
allows internal state changes through methods like
getAndAdd
. This is why AtomicLong
is ideal for
streams: it encapsulates mutable state in a thread-safe way without requiring
reassignment of the val
reference. Each call to
getAndAdd(2)
atomically updates the internal long
value, generating the sequence [2, 4, 6, ...] without violating the
final/effectively final rule.
If you tried using a regular long
variable (e.g., long counter = 2;
) and updated it within the lambda
(e.g., () -> counter += 2
), it would fail to compile because
counter
would no longer be effectively final. Modifying a non-final
variable inside a lambda is not allowed. AtomicLong
sidesteps this
by managing the state internally while keeping the reference to the
AtomicLong
object unchanged.
Athough the example uses a sequential stream, AtomicLong
ensures
thread-safety if the stream is made parallel (e.g.,
Stream.generate(...).parallel()
). The atomic operations prevent
race conditions when multiple threads access and update val
.
Without AtomicLong
, managing state in parallel streams would
require complex synchronization, which is error-prone.
For simple arithmetic sequences like this, Stream.iterate(2L, n -> n + 2).limit(20)
could be used, as it avoids explicit state management.
However, AtomicLong
with Stream.generate
is more
flexible for complex state updates or when thread-safety is needed.
$ java Main.java [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40]
Generating UUIDs
The next example demonstrates generating random UUIDs using the
Stream.generate
method. UUIDs (Universally Unique Identifiers) are
commonly used for unique identifiers in distributed systems and databases.
void main() { Stream.generate(UUID::randomUUID) .limit(3) .forEach(System.out::println); }
This example generates random UUIDs using the UUID.randomUUID method as the Supplier. Each call to the Supplier produces a new random UUID.
$ java Main.java a3e4f1b2-5c6d-4e3f-9g8h-7i6j5k4l3m2n b4c5d6e7-f8g9-h0i1-j2k3-l4m5n6o7p8q9 c2d3e4f5-g6h7-i8j9-k0l1-m2n3o4p5q6r7
Simulating sensor data
The next example simulates sensor readings using the generate method. It demonstrates how to create a stream of sensor data with different types and distributions. This can be useful for testing applications that process sensor data or for generating realistic data for simulations.
record SensorReading(String sensorId, double value, long timestamp) {} void main() { Random random = new Random(); String[] sensorIds = {"temp-1", "temp-2", "pressure-1", "humidity-1"}; Stream.generate(() -> { String id = sensorIds[random.nextInt(sensorIds.length)]; double value = switch(id.split("-")[0]) { case "temp" -> 20 + random.nextGaussian() * 5; case "pressure" -> 1000 + random.nextGaussian() * 10; case "humidity" -> 50 + random.nextGaussian() * 5; default -> 0; }; return new SensorReading(id, value, System.currentTimeMillis()); }) .limit(8) .forEach(System.out::println); }
This example simulates sensor readings with different distributions based on sensor type. The Supplier creates random values with realistic distributions for each sensor type.
$ java Main.java SensorReading[sensorId=temp-2, value=18.463, timestamp=1625761234567] SensorReading[sensorId=pressure-1, value=1005.382, timestamp=1625761234568] SensorReading[sensorId=humidity-1, value=52.174, timestamp=1625761234569] ...
Combining with other operations
Using generate with other stream operations.
void main() { var random = new Random(); Stream.generate(() -> random.nextInt(100)): .filter(n -> n > 50) .distinct() .limit(5) .sorted() .forEach(System.out::println); }
This example generates random numbers, filters for values greater than 50, removes duplicates, limits to 5 results, sorts them, and prints. It shows how generate can be combined with other stream operations.
$ java Main.java 53 67 72 84 91
Source
Java Stream generate documentation
In this article we have explored the Java Stream generate method. It provides a powerful way to create infinite streams of data, which can be useful for generating test data, simulations, and other scenarios where you need a continuous source of values.
Author
List all Java tutorials.