Appearance
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.io | java.util.stream |
---|---|
Storage | Sequentially reads/writes bytes or characters |
Purpose | Serialization 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.List | java.util.stream |
---|---|
Elements | Allocated and stored in memory |
Purpose | Operates 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 anotherStream
without modifying the originalStream
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:
- Create a Stream.
- Perform multiple transformations.
- 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.