Skip to content

Using Streams

Starting from Java 8, not only were Lambda expressions introduced, but a brand new stream API was also added: Stream API, located in the java.util.stream package.

Important Note: This Stream is different from java.io's InputStream and OutputStream; it represents a sequence of arbitrary Java objects. Here’s a comparison:

java.iojava.util.stream
StorageSequentially reads/writes bytes or characters
PurposeSerialization to files or networks

Some may wonder: isn’t a sequence of Java objects just a List container?

Key Point: This Stream is not the same as a List. In a List, each element is a Java object that is already stored in memory, whereas elements outputted by a Stream may not be pre-stored in memory and can be computed in real-time.

In other words, the purpose of a List is to manipulate a group of already existing Java objects, while a Stream implements lazy computation. Here’s a comparison:

java.util.Listjava.util.stream
ElementsAllocated and stored in memory
PurposeOperates on a group of existing Java objects

Understanding Streams

The concept of Stream can be a bit confusing, but an example will clarify it.

Suppose we want to represent the collection of all natural numbers. Clearly, it is impossible to do this with a List, as natural numbers are infinite, and no amount of memory can store them:

java
List<BigInteger> list = ??? // All natural numbers?

However, we can achieve this with a Stream:

java
Stream<BigInteger> naturals = createNaturalStream(); // All natural numbers

We won’t discuss how createNaturalStream() is implemented for now; let’s focus on how to use this Stream.

First, we can square each natural number, converting this Stream into another Stream:

java
Stream<BigInteger> naturals = createNaturalStream(); // All natural numbers
Stream<BigInteger> streamNxN = naturals.map(n -> n.multiply(n)); // Squares of all natural numbers

Since this streamNxN also contains an infinite number of elements, to print it, we must first limit it to a finite number. We can use the limit() method to get the first 100 elements and then use forEach() to process each element:

java
Stream<BigInteger> naturals = createNaturalStream();
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
        .limit(100)
        .forEach(System.out::println);

Characteristics of Streams

Let’s summarize the characteristics of Stream:

  • A Stream can "store" a finite or infinite number of elements. The word "store" is in quotes because the elements may be fully stored in memory or computed on demand.
  • Another characteristic of Stream is that it can easily transform into another Stream without modifying the original Stream itself.

Finally, actual computation typically happens when the final result is retrieved, which exemplifies lazy computation:

java
Stream<BigInteger> naturals = createNaturalStream(); // No computation
Stream<BigInteger> s2 = naturals.map(n -> n.multiply(n)); // No computation
Stream<BigInteger> s3 = s2.limit(100); // No computation
s3.forEach(System.out::println); // Computation occurs here

Lazy computation means that when a Stream is transformed into another Stream, only the transformation rules are stored, and no computation occurs until necessary. For instance, creating a Stream of all natural numbers does not trigger computation, and the same goes for transforming it into s2 or s3. Only when we invoke forEach, which requires the output elements of the Stream, does computation take place.

We typically write Stream operations in a chain for more concise code:

java
createNaturalStream()
    .map(n -> n.multiply(n))
    .limit(100)
    .forEach(System.out::println);

Basic Usage of Stream API

Thus, the basic usage of the Stream API is:

  1. Create a Stream.
  2. Perform multiple transformations.
  3. Call a terminal operation to obtain the actual computed result:
java
int result = createNaturalStream() // Create Stream
             .filter(n -> n % 2 == 0) // Any number of transformations
             .map(n -> n * n) // Any number of transformations
             .limit(100) // Any number of transformations
             .sum(); // Final computation result

Summary

The characteristics of the Stream API are:

  • The Stream API provides a new abstract sequence for stream processing.
  • The Stream API supports functional programming and chaining operations.
  • Streams can represent infinite sequences and, in most cases, are evaluated lazily.
Using Streams has loaded