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
1. Data Processing: Streams provide a clean way to process collections of objects without writing boilerplate loops.
2. Functional Operations: They support operations such as filter, map, reduce, and collect to transform and analyze data smoothly.
3. Parallel Execution: Streams can run tasks in parallel using parallelStream, helping improve performance for large datasets.
4. Cleaner and Readable Code: Stream pipelines make code more expressive, readable, and easier to maintain.
5. Lazy Evaluation: Intermediate operations are evaluated only when needed, improving efficiency for large collections.
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 Operations
- Terminal Operations.
Intermediate operations are the operations in a stream pipeline that transform the data but do not produce the final output immediately. Instead, they return another stream, allowing several methods to be connected one after another.
Key characteristics of intermediate operations
- They run only when a terminal operation is called because streams follow the idea of lazy execution.
- They build a chain where the output of one method becomes the input of the next.
- Each operation produces a new stream instead of a final value.
- They support transformations like filtering, mapping, sorting, flattening, and more.
Terminal Stream Operations in Java
Terminal operations are the last step in a stream pipeline. They produce a final result or trigger side effects and do not return another stream. These operations force all the intermediate operations to execute because of Java Streams’ lazy evaluation
Key Characteristics of Terminal Operations
- Examples include aggregation, iteration, and matching operations.
- They end the stream pipeline.
- They return a final value, a collection, or an act rather than producing another stream.
- They execute all pending intermediate operations.
- map: Transforms each element by applying a function and produces a new stream of the modified values.
- filter: Keeps only the elements that match a given condition and drops the rest.
- sorted: Arranges the stream elements in their natural order or based on a given comparator.
- flatMap: Opens nested structures such as lists inside lists and turns them into a single continuous stream.
- distinct: Removes repeated values and keeps unique elements.
- peek: Let’s you act on each element for inspection purposes while keeping the stream unchanged.
Common Terminal Operations in Java Streams
- collect: Gathers the processed elements into a collection such as a list, set, or map.
- forEach: Acts on each element of the stream, typically for printing or logging.
- reduce: Combines all elements of a stream into a single value using a binary operation.
- count: Returns the total number of elements in the stream.
- findFirst: Retrieves the first element of the stream if it exists.
- allMatch: Checks if every element satisfies a given condition.
- anyMatch: Checks if at least one element satisfies a given condition
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:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Adam", "Eve", "Mike");
List<String> result = names.stream()
.filter(n -> n.startsWith("J"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result);
}
}
Output:
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?
Yes, a Java stream can be null if you assign the value null to the stream variable.
But if you try to call any stream operation on a null reference, Java will throw a NullPointerException.
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.