In programming, evaluation strategy determines when expressions or computations are executed. Two common strategies are eager (or strict) and lazy evaluation.
In eager evaluation, expressions are computed immediately when they are encountered. This means all data processing happens upfront, regardless of whether the results are actually used later.
Example with eager evaluation using collections:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class EagerExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Eagerly process the collection
List<Integer> doubled = numbers.stream()
.map(n -> {
System.out.println("Doubling " + n);
return n * 2;
})
.collect(Collectors.toList()); // Terminal operation triggers processing
System.out.println(doubled);
}
}
Here, the entire list is processed immediately when collect()
is called, doubling each number and printing the operation for every element.
Lazy evaluation defers computation until the results are actually needed. This is a key concept in functional programming, enabling efficient use of CPU and memory by avoiding unnecessary work.
Java’s Streams API supports lazy evaluation for intermediate operations like map()
, filter()
, or sorted()
. These operations build a pipeline but do not process elements until a terminal operation (e.g., forEach()
, collect()
, findFirst()
) is invoked.
Example with lazy evaluation:
import java.util.Arrays;
import java.util.List;
public class LazyExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.map(n -> {
System.out.println("Mapping " + n);
return n * 2;
})
.filter(n -> {
System.out.println("Filtering " + n);
return n > 5;
})
.forEach(n -> System.out.println("Consumed " + n));
}
}
Output shows that mapping and filtering happen only as needed, per element:
Mapping 1
Filtering 2
Mapping 2
Filtering 4
Mapping 3
Filtering 6
Consumed 6
Mapping 4
Filtering 8
Consumed 8
Mapping 5
Filtering 10
Consumed 10
Notice the operations interleave and stop early if possible, avoiding processing the entire collection upfront.
Infinite streams are streams without a predefined end—they can generate an unbounded sequence of elements. Unlike collections with fixed size, infinite streams rely on lazy evaluation to produce elements on demand, making it possible to work with potentially limitless data safely and efficiently.
Java’s Streams API provides two primary methods for creating infinite streams:
Stream.iterate(seed, UnaryOperator)
: Generates an infinite stream by repeatedly applying a function to the previous element.Stream.generate(Supplier)
: Produces an infinite stream by repeatedly calling a supplier function that generates elements independently.Because of lazy evaluation, no elements are computed until the stream is consumed through a terminal operation.
Stream.iterate
Generate an infinite sequence of natural numbers starting at 1:
import java.util.stream.Stream;
public class InfiniteStreamIterate {
public static void main(String[] args) {
Stream<Integer> naturalNumbers = Stream.iterate(1, n -> n + 1);
// Limit to first 10 elements to avoid infinite processing
naturalNumbers.limit(10)
.forEach(System.out::println);
}
}
Output:
1
2
3
4
5
6
7
8
9
10
Here, iterate
builds an infinite stream, but limit(10)
safely restricts the output to the first 10 elements, preventing unbounded computation.
Stream.generate
Generate an infinite stream of random numbers:
import java.util.Random;
import java.util.stream.Stream;
public class InfiniteStreamGenerate {
public static void main(String[] args) {
Random random = new Random();
Stream<Double> randomNumbers = Stream.generate(random::nextDouble);
randomNumbers.limit(5)
.forEach(System.out::println);
}
}
This creates an endless stream of random doubles, but only 5 are printed due to limit
.
Without lazy evaluation, infinite streams would cause immediate non-termination or out-of-memory errors, since all elements would be computed at once. Java streams defer element generation until a terminal operation requests them, enabling infinite streams to be practical and safe.
Java’s Streams API makes infinite streams accessible via Stream.iterate
and Stream.generate
. Thanks to lazy evaluation, these streams produce elements only as needed, allowing safe processing with short-circuiting operations like limit()
. Infinite streams empower powerful functional programming patterns to model unbounded or on-demand data sources flexibly and efficiently.
The Fibonacci sequence is a classic example of an infinite numeric series where each number is the sum of the two preceding ones, starting from 0 and 1:
0, 1, 1, 2, 3, 5, 8, 13, 21, ...
Using Java’s infinite streams, we can generate this sequence lazily—producing values only when requested, avoiding unnecessary computation or memory usage.
Stream.iterate
Here’s a runnable example generating Fibonacci numbers lazily:
import java.util.stream.Stream;
public class LazyFibonacci {
public static void main(String[] args) {
// Stream.iterate takes a seed (initial pair) and a function to produce the next pair
Stream<long[]> fibStream = Stream.iterate(
new long[]{0, 1}, // Initial pair: fib(0)=0, fib(1)=1
pair -> new long[]{pair[1], pair[0] + pair[1]} // Next pair: [fib(n), fib(n+1)]
);
// Extract only the first element of each pair (the current Fibonacci number),
// limit to first 15 numbers to avoid infinite output
fibStream
.limit(15)
.map(pair -> pair[0])
.forEach(System.out::println);
}
}
The stream’s seed is a two-element array representing the first two Fibonacci numbers [0, 1]
.
The unary operator function generates the next pair by shifting the previous pair:
pair[1]
becomes the new first element.pair[0] + pair[1]
becomes the new second element.This way, each step produces a pair representing consecutive Fibonacci numbers.
The stream is infinite because Stream.iterate
will keep generating pairs indefinitely.
We use .limit(15)
to safely restrict output to the first 15 Fibonacci numbers.
.map(pair -> pair[0])
extracts the current Fibonacci number from each pair.
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
.limit()
protects against infinite looping or excessive memory use, demonstrating safe consumption of infinite streams.This pattern illustrates the power of Java streams combined with functional programming concepts to elegantly and efficiently generate infinite sequences on demand.