You are currently viewing Creating and Consuming Streams in Java

Creating and Consuming Streams in Java

This entry is part 5 of 5 in the series Modern Java Features (Java 8+)

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:

  1. You can only consume a stream once.
  2. 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 arrays
  • Stream.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 demand
  • Stream.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 elements
  • collect(...) — 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:

  1. Streams are lazy: intermediate operations describe work.
  2. 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.

Modern Java Features (Java 8+)

Java Stream API: What It Is (and What It Is Not)

Noel Kamphoa

Experienced software engineer with expertise in Telecom, Payroll, and Banking. Now Senior Software Engineer at Societe Generale Paris.