Key Takeaways
- Java Stream API, introduced in Java 8, simplifies data processing with a functional approach.
- Java Streams allow operations like filter, map, reduce, and collect without modifying the original data.
- Intermediate operations in Java Streams are lazy, and terminal operations trigger execution.
- Parallel Streams in Java improve performance for large datasets by using multiple CPU cores.
- Mastering Java Streams helps developers write clean, efficient, and modern Java code.
Java stream, or streams in Java, is a feature introduced in Java 8, which enhanced data processing by offering a functional and declarative approach. Java Streams help developers to perform sequential or parallel aggregate operations on sequences of objects. The addition of the Stream API in Java was a significant enhancement as it facilitates the manipulation and transformation of data. In Java 9, several improvements were made to this feature, making it more capable and refined in its functionality. This stream in Java tutorial will explore both the original Stream API and the enhancements introduced in Java 9, focusing on practical examples to help you understand its usage. For understanding this tutorial, you should have a basic working knowledge of Java 8 (lambda expressions, optional, method references).
Table of Contents:
What is Java Stream?
“Java Stream is a sequence of elements from a source that supports aggregate operations (e.g., filter, map, reduce, collect).”
Java Stream is quite different from Java I/O Streams in terms of its features and operations (e.g., FileInputStream). It is designed to facilitate data processing operations in a smooth way. Java Streams act as wrappers around data sources (like collections, arrays, or I/O channels), facilitating functional-style operations without altering the underlying data.
Use of Java Streams
The uses of Java Streams are mentioned below:
- Stream API is a way to express and process collections of objects.
- Streams help us to perform operations like filter, map, reduce, collect, and more.
Java Stream Features
Let’s have a look at its main features:
- Consumable: A stream can only be used once.
- Not a data structure: Streams do not store elements; they are computed on demand.
- Functional-style operations: Streams provide support for functional-style operations, such as map, filter, and reduce, which allow programmers to express data processing logic in a more declarative and natural way.
- Pipelining: Streams enable the easy chaining of operations in a pipeline, where the output of one operation is passed directly to the next operation. This makes the code feel more efficient and compact, eliminating the need for intermediate collections.
- Lazy evaluation: Streams employ lazy evaluation as intermediate operations are executed only when the terminal operation is invoked. This improves efficiency by avoiding unnecessary computation.
- Parallel processing: Parallel streams automatically split data into numerous process chunks and process them concurrently using different CPU cores. This improves performance for the large datasets.
How to Create a Java Stream?
In Java, there are several ways to create a stream. You can create an empty stream, a stream from an array, or a stream from specific values, among other options. The basic declaration of a stream looks like this:
Stream<T> stream;
Here, T represents the type of elements in the stream, which can be a class, object, or primitive type depending on what data you want to work with.
Get 100% Hike!
Master Most in Demand Skills Now!
Types of Stream Operations in Java
Streams in Java are not data structures but tools that facilitate performing operations like map and reduce transformations on collections. The “java.util.stream” supports functional-style operations on streams of elements.
Let’s understand the types of Stream operations in Java, where we primarily have two operations – Intermediate and Terminal operations.
- Intermediate operations: These transform or filter the data and return another stream. These are lazy and can execute only when a terminal operation has been called.
Examples include: map(), filter(), sorted(), distinct().
- Terminal operations: These mainly mark the end of the stream pipeline, and they produce either a result or a side effect.
Examples include: collect(), forEach(), reduce(), count().
To execute the above operations, you need a stream source. A stream originates from the stream source and defines the data source for the pipeline. Streams can be created from arrays, or methods like Stream.of(), Arrays.stream(), or List.stream().
Java Stream Operations:
Stream.of(5, 4, 3, 2, 1) // Stream source
.filter(x -> x % 2 == 0) // Intermediate operation
.collect(Collectors.toList()) // Terminal operation
In this example, a stream is created using Stream.of(5, 4, 3, 2, 1) as the data source. The intermediate operation filter(x -> x % 2 == 0) filters out all the odd numbers, keeping only the even ones. Finally, the terminal operation collect(Collectors.toList()) gathers the filtered results into a list. As a result, the stream processes the elements declaratively and produces the list [4, 2].
Please note that Stream Source is not an operation, yet it is important for other operations to work properly.
Example of Java Streams
Consider a list of names:
List<String> names = Arrays.asList("John", "Jane", "Adam", "Eve", "Mike");
Here we can use a stream to process this list by filtering names that start with “J” and then converting them to uppercase such as shown below:
List<String> result = names.stream()
.filter(n -> n.startsWith("J"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result); // Output: [JOHN, JANE]
Now that a stream is created from our names list. The filter operation only selects the names starting with “J”, and the map operation transforms them to uppercase. Lastly, collect gathers the processed elements into a new list. The output shows the transformed names [JOHN, JANE].
Java Streams With Different Operations
Here’s an example of Java 8 Stream API to understand this:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
// Original list of names
List<String> names = Arrays.asList("John", "Jane", "Adam", "Eve", "Mike");
// Filter names starting with 'J'
List<String> jNames = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
System.out.println("Names starting with 'J': " + jNames);
// Map to lengths of names
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("Lengths of names: " + nameLengths);
// Sort names alphabetically
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("Sorted names: " + sortedNames);
// Total length of all names
int totalLength = names.stream()
.mapToInt(String::length)
.sum();
System.out.println("Total length of all names: " + totalLength);
}
}
Master Java Programming with Intellipaat.
Become an Industry-ready Java developer with in-demand skills!
Java Stream Interface Methods
| Method |
Description |
| distinct() | Returns a stream with only unique elements. |
| findAny() | Returns any element from the stream as an Optional. |
| forEach(action) | Performs an action for each element in the stream. |
| limit(maxSize) | Returns a stream with at most maxSize elements. |
| peek(action) | Performs an action on each element while keeping the stream unchanged. |
| sorted() | Returns a stream with elements in natural order. |
| toArray() | Converts stream elements into an array. |
| anyMatch(predicate) | Returns true if any element matches the given condition. |
| allMatch(predicate) | Returns true if all elements match the given condition. |
| collect(collector) | Collects stream elements into a collection or result. |
| builder() | Creates a builder to construct a stream. |
| concat(a, b) | Combines two streams into one. |
Conclusion
Java Streams provide a functional, declarative way to process collections in Java. With intermediate and terminal operations, lazy evaluation, and parallel processing, streams make data manipulation efficient and readable. Mastering Java Streams helps developers write cleaner code and handle large datasets effectively.
Java Stream – FAQs
1. Can a Java stream be null?
No, a Java Stream itself can not be null. If a variable holding a stream is null, then attempting any stream operation will throw a NullPointerException. Therefore it is better to create a stream from a non-null source like a collection, array, or Stream.of().
2. Are Java streams push or pull?
Java Streams are pull-based, meaning elements are requested and pulled through the pipeline when a terminal operation is invoked, rather than being pushed automatically from the source.
3. Are Java streams synchronous?
By default, Java Streams are synchronous, meaning operations execute sequentially in a single thread. However, using parallel streams (parallelStream()), they can process elements concurrently across multiple threads.
4. Why are streams lazy in Java?
Java Streams are lazy because intermediate operations (like filter, map, sorted) do not process data immediately. They are executed only when a terminal operation (like collect, forEach, reduce) is invoked, which improves performance by avoiding extra computations.
5. Why do streams skip?
Streams in java “skip” elements when the skip(n) intermediate operation is used. It ignores the first n elements of the stream and passes the remaining elements, allowing selective processing without modifying the original data.