Published: July 22, 2016
Introduction
Java 8 introduced the powerful Streams API, which makes it easier to process sequences of data in a functional style. In this tutorial, we will dive into the basics of streams, explaining how to create and manipulate streams to perform operations on data in a more concise and readable manner.
We will cover:
- What streams are and why they are important.
- How to create and use streams.
- How to combine stream operations to process data effectively.
By the end of this tutorial, you’ll have a good understanding of how to use streams in your applications.
What Is a Stream?
In Java, a Stream represents a sequence of elements supporting sequential and parallel aggregate operations. The key difference between a stream and a collection (like a List or Set) is that streams do not store data; they only provide methods to manipulate the data they receive from a source (such as a collection, array, or I/O channel).
Streams are part of Java’s functional programming paradigm and are typically used to perform operations such as filtering, mapping, and reducing on data collections.
Creating a Stream
You can create a stream from any Collection (like a List or Set), an array, or a stream builder. Let’s look at some common ways to create streams.
1. Creating a Stream from a Collection
import java.util.List;
import java.util.Arrays;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill", "James");
// Create a stream from the list
names.stream()
.forEach(System.out::println); // Output each name
}
}
In the example above, we create a stream from a list of names and use forEach
to print each name.
2. Creating a Stream from an Array
public class StreamExample {
public static void main(String[] args) {
String[] names = {"John", "Jane", "Jack", "Jill", "James"};
// Create a stream from the array
Arrays.stream(names)
.forEach(System.out::println); // Output each name
}
}
Stream Operations
Streams allow you to chain together multiple operations to manipulate and process data. These operations fall into two categories: intermediate and terminal operations.
1. Intermediate Operations
Intermediate operations are lazy operations that return a new stream and are used to transform or filter data. Some common intermediate operations include:
- map(): Transforms each element.
- filter(): Filters elements based on a condition.
- distinct(): Removes duplicates.
- sorted(): Sorts elements.
Example 1: Using map() to Transform Data
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Double each number in the list using map()
numbers.stream()
.map(n -> n * 2)
.forEach(System.out::println); // Outputs: 2, 4, 6, 8, 10
}
}
2. Terminal Operations
Terminal operations are operations that trigger the processing of the stream and produce a result. Once a terminal operation is executed, the stream is consumed and can no longer be used. Common terminal operations include:
- forEach(): Iterates over each element.
- collect(): Collects the stream elements into a collection (List, Set, etc.).
- reduce(): Aggregates elements using a binary operator.
Example 2: Using filter() to Filter Data
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill", "James");
// Filter names starting with 'J'
names.stream()
.filter(name -> name.startsWith("J"))
.forEach(System.out::println); // Outputs: John, Jane, Jack, Jill, James
}
}
Example 3: Using collect() to Collect Stream Results
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill", "James");
// Collect filtered names into a new list
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
System.out.println(filteredNames); // Outputs: [John, Jane, Jack, Jill, James]
}
}
Combining Stream Operations
Streams can be chained together to perform complex operations on data in a clear and concise way.
Example 4: Chaining Multiple Operations
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill", "James");
// Chain multiple operations: filter, map, and collect
List<String> result = names.stream()
.filter(name -> name.startsWith("J")) // filter names starting with 'J'
.map(String::toUpperCase) // convert names to uppercase
.collect(Collectors.toList()); // collect the results into a list
System.out.println(result); // Outputs: [JOHN, JANE, JACK, JILL, JAMES]
}
}
Stream Pipelines
A stream pipeline is a sequence of stream operations that can be executed either sequentially or in parallel. A pipeline consists of:
- A source (like a collection or array).
- Zero or more intermediate operations (such as
map()
orfilter()
). - A terminal operation (such as
collect()
orforEach()
).
In the above example, the stream pipeline is formed by chaining filter()
, map()
, and collect()
operations.
Parallel Streams
One of the most powerful features of the Streams API is parallelism. Streams can be processed in parallel to improve performance when dealing with large data sets. You can convert a stream to a parallel stream by calling .parallel()
on the stream.
Example 5: Using Parallel Streams
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Process the list in parallel
numbers.parallelStream()
.map(n -> n * 2)
.forEach(System.out::println); // Outputs in parallel
}
}
Note: Parallel streams are useful for large collections but might not always be faster, especially for small data sets. It’s important to benchmark parallel streams in your use case.
Conclusion
Java 8’s Streams API is a powerful tool for processing collections in a functional programming style. With stream operations like map()
, filter()
, and collect()
, you can process data in a more concise and readable manner than with traditional loops.
In the next tutorial, we will explore more advanced operations like collecting results into different data structures and using Collectors.