Mastering Java Streams
Introduction to Java Streams
A New Way to Handle Data
Before Java 8, if you had a list of items and wanted to perform some action on a subset of them, you'd probably reach for a loop. For instance, imagine you have a list of numbers and you only want to find the even ones.
import java.util.ArrayList;
import java.util.List;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
// evenNumbers now contains [2, 4, 6, 8]
This code works, but it's a bit clunky. It involves creating a new list, explicitly iterating through the original list, checking a condition, and adding elements one by one. This is an imperative style of programming: you're telling the computer exactly how to do the task.
Java Streams, introduced in Java 8, offer a more elegant, declarative approach. A stream is a sequence of elements from a source that supports aggregate operations. Think of it like an assembly line for your data. You set up a series of stations (operations), and the data flows through them to produce a final result.
With Java 8 streams you can extract extra specific, relevant data from a large source with one line of code.
This approach lets you focus on what you want to achieve, rather than the nitty-gritty details of how to do it. The result is often cleaner, more readable code.
Streams vs Collections
It's easy to confuse streams with collections like ArrayList, but they are fundamentally different. A collection is a data structure; its main purpose is to store and group elements. You can add items, remove them, and access them multiple times.
A stream, on the other hand, doesn't store data. It carries values from a source, like a collection, through a pipeline of operations. Here are the key distinctions:
| Feature | Collection | Stream |
|---|---|---|
| Data Storage | Stores elements. | Does not store elements. |
| Consumption | Can be traversed multiple times. | Can only be traversed once. |
| Evaluation | Eagerly computed. | Lazily evaluated. |
The one-time use aspect is important. Once a stream has been consumed by a final operation, it's closed. If you want to process the same data again, you need to create a new stream from the source.
The concept of lazy evaluation means that intermediate operations are not performed until a final, terminal operation is invoked. This allows for significant optimizations behind the scenes.
The Stream Pipeline
Working with streams typically involves a three-stage pipeline. It’s like a factory assembly line for your data.
Let's break down each part:
-
Source: This is where the stream originates. It can be a collection (like a
ListorSet), an array, or an I/O channel. -
Intermediate Operations: These are operations that transform the stream into another stream. Examples include
filter()(to select elements based on a condition) andmap()(to transform each element). You can chain multiple intermediate operations together. These operations are lazy; they don't actually do any work until the final step. -
Terminal Operation: This is the operation that kicks off the processing and closes the stream. It produces a result, like a new collection, a single value (e.g., a count), or simply performs an action on each element (e.g., printing to the console).
A stream pipeline will not execute until a terminal operation is called. It's the trigger for the entire chain of events.
Let's revisit our earlier example of filtering even numbers. Here’s how you would write it using a stream:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> evenNumbers = numbers.stream() // 1. Source
.filter(n -> n % 2 == 0) // 2. Intermediate operation
.collect(Collectors.toList()); // 3. Terminal operation
// evenNumbers now contains [2, 4, 6, 8]
Notice how this code describes what we want: get a stream from the list, filter it to keep only even numbers, and then collect the results into a new list. It's concise, readable, and less prone to errors than the manual loop. This declarative style is the core benefit of using streams.
Ready to check your understanding? Let's see what you've learned about the basics of Java Streams.
What is the primary programming style that Java Streams encourage?
Which of the following is a key characteristic of a Java Stream?
You now have a solid foundation for what streams are and why they're a powerful tool in Java. They provide a clean and efficient way to process collections of data.