- Local Variable Type Inference in Java
- Sealed Classes and Interfaces In Java
- Records In Java
- Java Stream API: What It Is (and What It Is Not)
- Creating and Consuming Streams in Java
Introduction
After clarifying what the Java Stream API is (a computation pipeline) and what it is not (a collection), the next step is to learn how to create streams and how to consume them correctly.
In practice, most bugs and confusion around streams come from two simple facts:
- You can only consume a stream once.
- A stream executes only when you invoke a terminal operation.
This article focuses on stream sources (how to obtain a stream) and terminal operations (how to consume it). It intentionally stays light on advanced topics such as pipeline optimization, reduce(), parallel streams, or Spliterator, which are covered in dedicated articles.
“A stream is a one-shot view: you describe a computation once, then you consume it once.”
Internal reading suggestion: If you have not read the conceptual overview yet, start with Java Stream API: What It Is (and What It Is Not) before continuing.
1. Stream Creation From Collections
The most common way to create a stream is from a Collection. This is the “happy path” because collections already expose stream() (sequential) and parallelStream() (parallel).
The snippet below builds a stream from a list and consumes it with a terminal operation.
List<String> names = List.of("Alice", "Bruno", "Chloé", "David");
long count = names.stream()
.filter(n -> n.length() <= 5)
.count();
System.out.println("Names with length <= 5: " + count);// Names with length <= 5: 4
Key idea: filter(...) does not run immediately. The pipeline executes only when count() is called.
2. Stream Creation From Arrays
Arrays are not collections, but Java provides convenient entry points:
Arrays.stream(array)for object or primitive arraysStream.of(...)for varargs(See the next section)
The following snippet demonstrates the first approach. The second approach is shown in the next section.
String[] langs = {"Java", "Kotlin", "Scala", "Groovy"};
String joined = Arrays.stream(langs)
.map(String::toUpperCase)
.collect(Collectors.joining(", "));
System.out.println(joined);//JAVA, KOTLIN, SCALA, GROOVY
Tip: prefer Arrays.stream(...) when you already have an array variable. Use Stream.of(...) for inline creation.
3. Stream Creation Using Stream.of and Stream.empty
You may create Streams directly in this way when you have a small set of known elements.
This is useful for tests, quick demos, or bridging legacy APIs.
Stream<String> s1 = Stream.of("A", "B", "C");
Stream<String> s2 = Stream.empty();
System.out.println("s1 count = " + s1.count());// s1 count = 3
System.out.println("s2 count = " + s2.count());// s2 count = 0
Important: after calling count() on s1, it is consumed and cannot be reused.
4. Infinite Streams With Stream.generate and Stream.iterate
Some streams have no fixed size and can be infinite. Java supports this via:
Stream.generate(supplier)— generates elements on demandStream.iterate(seed, next)— produces a sequence
Infinite streams must be bounded with operations like limit(n) to avoid non-terminating pipelines.
The following code provides the first five even numbers:
List<Integer> firstFiveEven = Stream.iterate(0, n -> n + 2)
.limit(5)
.collect(Collectors.toList());
System.out.println(firstFiveEven);// [0, 2, 4, 6, 8]
“Infinite streams are safe only when you deliberately set a stopping rule.”
5. Streams From Files
Streams also shine when processing I/O, such as reading lines from a text file. Files.lines(Path) returns a stream that must be closed. The best practice is to use try-with-resources.
The following code reads a file and returns the number of lines containing the string “Java”:
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
long javaLines = lines
.filter(line -> line.contains("Java"))
.count();
System.out.println("Lines containing 'Java': " + javaLines);
}
Why try-with-resources matters: file streams hold OS resources. Closing them is not optional.
6. Consuming Streams With Terminal Operations
A stream pipeline becomes real only when you call a terminal. Common terminal operations include:
forEach(...)— consume elements (order not guaranteed in parallel)count()— count elementscollect(...)— build a list, set, map, etc.min(...)/max(...)findFirst()/findAny()anyMatch(...)/allMatch(...)/noneMatch(...)
Example: Collecting filtered values.
List<String> data = List.of("java", "spring", "angular", "docker");
List<String> filtered = data.stream()
.filter(s -> s.length() >= 6)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filtered);// [SPRING, ANGULAR, DOCKER]
Practical rule: use collect(...) when you need a new container (list, set, map). Use count() when you only need a number.
7. The Single-Use Rule: You cannot reuse a Stream
One of the most common runtime errors is attempting to use the same stream twice.
A stream is consumed after the first terminal operation.
Stream<String> stream = Stream.of("x", "y", "z");
System.out.println(stream.count());
// This will throw IllegalStateException:
// stream.forEach(System.out::println);
“A stream can only be consumed once.”
Best practice: store the source (collection, array, supplier), not the stream itself.
Conclusion
Creating and consuming streams is straightforward once you master these two rules:
- Streams are lazy: intermediate operations describe work.
- Streams are single-use: terminal operations consume the pipeline.
With these fundamentals, you are ready to study how to compose and optimize stream pipelines.
You can find the complete code of this article here on GitHub.
