Streams in Java- Complete Tutorial with Examples
Learn via video course
Overview
Stream is a sequence of objects that supports various sequential and parallel aggregate operations. Streams in Java, including the Stream API in Java 8, provide a powerful and efficient way to process data in a functional and declarative manner. The Stream API offers a set of stream classes in Java that allow for the manipulation and transformation of data. Understanding streams in Java is essential for modern Java developers to write efficient and expressive code.
See Also: Complete Java Tutorial
Introduction of Streams in Java
Streams in Java, introduced in Java 8, are a powerful addition to the Java API that allow for functional-style processing of collections and arrays. The Stream API in Java provides a high-level abstraction for processing data in a declarative and parallelizable manner, making code more concise and expressive.
Features of Streams in Java:
Functional-style operations: Streams support functional-style operations like map, filter, and reduce, which enable developers to express data processing logic in a more declarative and readable way.
Pipelining: Streams allow for chaining multiple operations together to form a pipeline, where the output of one operation becomes the input of the next. This enables efficient and compact code without the need for intermediate collections.
Lazy evaluation: Streams employ lazy evaluation, meaning that intermediate operations are not executed until a terminal operation is invoked. This improves efficiency by avoiding unnecessary computation.
Parallel processing: Streams can be processed in parallel, leveraging multi-core architectures to improve performance for large datasets. Parallel streams automatically divide the data into multiple chunks and process them concurrently.
Streams in Java provide a powerful and efficient way to process data by applying various sequential and parallel aggregate operations. The Stream API in Java 8 introduced the concept of streams and stream classes in Java, which offer specialized functionality for manipulating and transforming data. Developers can utilize the Stream API in Java to write code that is more concise, expressive, and efficient.
The Stream API in Java provides a rich set of operations that can be applied to streams, such as filtering, mapping, and reducing. The Stream API in Java provides a high-level abstraction for working with streams and offers a wide range of operations to manipulate and process data.
Example of Streams in Java:
Here's an example that demonstrates the use of Streams in Java to filter and transform a collection of names:
In this example, we create a stream from the names list, filter the names starting with "J", convert them to uppercase using the map operation, and collect the results into a new list using the collect terminal operation. The output is the filtered and transformed names: [JOHN, JANE].
Different Operations on Streams
Stream provides various operations that can be chained together to produce results. Stream operations can be classified into two types.
- Intermediate Operations
- Terminal Operations
1. Intermediate Operations
Intermediate operations return a stream as the output, and intermediate operations are not executed until a terminal operation is invoked on the stream. This is called lazy evaluation, and it is discussed in detail in the later section (Lazy Evaluation).
filter()
The filter() method returns a stream with the stream's elements that match the given predicate. Predicate is a functional interface in Java that accepts a single input and can return a boolean value.
Example
Output
Explanation This example filters the even values based on the predicate (value -> value % 2 == 0) passed to it.
map()
The map() method returns a stream with the resultant elements after applying the given function on the stream elements.
Example
Output
Explanation
In the example the map() method is called with the function value -> value * 10 on the stream. The function is called for all values of the stream, and hence the result contains all stream values multiplied by 10.
sorted()
The sorted() method returns a stream with the elements of the stream sorted according to natural order or the provided Comparator.
Example
Output
Explanation
- The sorted() method without any parameters sorts the elements in ascending order.
- The sorted() method with the comparator Comparator.reverseOrder() sorts the element in the descending order.
distinct()
This distinct() method returns a stream consisting of distinct elements of the stream (i.e.) it removes duplicate elements.
Example
Output
Explanation
The distinct() method removes the duplicate values 1 and 2.
peek()
The peek() method returns a stream consisting of the elements of the stream after performing the provided action on each element. This is useful when we want to print values after each intermediate operation.
Example
Output
Explanation
We printed the filtered values using the peek() method after the intermediate filter() operation.
limit()
The limit() method returns a stream with the stream elements limited to the provided size.
Example
Output
Explanation
We limited the size of the stream to 3 using the limit(3) method.
skip()
This skip() method returns a stream consisting of the stream after discarding the provided first n elements.
Example
Output
Explanation The first two elements are skipped using the skip(2) method.
2. Terminal Operations
Terminal operations produce the results of the stream after all the intermediate operations are applied, and we can no longer use the stream once the terminal operation is performed. forEach()
The forEach() method iterates and performs the specified action for each stream element. For parallel stream, it doesn't guarantee to maintain the order of the stream.
Example
Output
Explanation
The forEach() method iterates and prints all the stream values.
forEachOrdered()
The forEachOrdered() method iterates and performs the specified action for each stream element. This is similar to the forEach() method, and the only difference is that it maintains the order when the stream is parallel.
Output
Explanation
From the output, we can see that the forEach() method doesn't maintain the order of the stream, whereas the forEachOrdered() method maintains the order of the stream. collect() The collect() method performs a mutable reduction operation on the elements of the stream using a Collector.
Mutable Reduction
A mutable reduction is an operation in which the reduced value is a mutable result container, like an ArrayList.
Collector
A Collector is a class in Java that implements various reduction operations such as accumulating elements into collections, summarizing elements, etc.
Example
Output
Explanation
The filter(x -> x % 2 == 0) method return a stream that contains even numbers. The stream is then converted to a list via collect(Collectors.toList()).
count()
The count() method returns the total number of elements in the stream.
Example
Output
Explanation
The count() method returns the total number of elements in the stream, 5.
reduce()
The reduce() method performs a reduction on the elements of the stream and returns the value.
Example
Output
Explanation
- The reduce() method is called with two arguments, an initial value (0) and the accumulator method (value, sum) -> sum += value).
- Each stream element will be added to the previous result to produce the sum.
toArray()
The toArray() method returns an array that contains the elements of the stream.
Example
Output
Explanation
The toArray() method returns an array with the elements of the stream.
min() and max()
The min() and max() methods return an Optional that contains the minimum and maximum elements of the stream, respectively, according to the provided comparator.
Example
Output
Explanation
The min() and max() methods return optionals that contain the minimum and maximum elements of the stream respectively. The comparator used to compare the elements is (a, b) -> Integer.compare(a, b). This comparator returns 0 if a == b, a negative value if a < b and, a positive value if a > b.
findFirst()
The findFirst() method returns an Optional that contains the first element of the stream or an empty Optional if the stream is empty.
Example
Output
Explanation The findFirst() method returns an Optional with the first element of the stream, which is 1.
findAny()
The findAny() method returns an Optional containing some element of the stream or an empty Optional if the stream is empty.
Example
Output
Explanation
The findAny() method can return any element in this stream. In this example, it returns 1. noneMatch() When no stream elements match the specified predicate, the noneMatch() method returns true, otherwise false. If the stream is empty, it returns true.
Example
Output
Explanation
- The first noneMatch(value -> value == 2) returns false because there is a 2 in the stream.
- The second noneMatch(value -> value == 0) returns true because there is no 0's in the stream.
allMatch()
When all the stream elements meet the specified predicate, the allMatch() method returns true, otherwise false. If the stream is empty, it returns true.
Example
Output
Explanation
The allMatch(value -> value == 2) returns false because there are other elements in the stream apart from 2.
anyMatch()
When any stream element matches the specified predicate, the anyMatch() method returns true, otherwise false. If the stream is empty, it returns false.
Example
Output
Explanation
The anyMatch(value -> value == 2) returns true because there is a 2 in the stream.
Different Ways to Create Streams in Java
Java streams can be created in many ways, including creating an empty stream, creating a stream from an existing array, creating a stream from specified values, etc.
Stream.empty()
Stream.empty() creates an empty stream without any values. This avoids null pointer exceptions when calling methods with stream parameters. We create empty streams when we want to add objects to the stream in the program.
Syntax
Example
Output
Explanation
The Stream.empty() method created an empty stream without any values in it. Hence calling count() method on the stream returned 0.
Stream.builder()
Stream.builder() returns a builder (a design pattern that allows us to construct an object step-by-step) that can be used to add objects to the stream. The objects are added to the builder using the add() method. Calling build() method on the builder creates an instance of the Stream. This implementation of Stream creation is based on the famous Builder Design Pattern.
Syntax
Example
Output
Explanation
The Stream.builder() method returned a Stream.Builder instance and it is used to add strings to the builder instance. The Stream instance is created from the builder by calling the build() method.
Stream.of()
Stream.of() method creates a stream with the specified values. This method accepts both single and multiple values. It does the work of declaring and initializing a stream.
Syntax
Example
Output
Explanation
We created two streams stream1 and stream2 by passing single (1) and multiple values (1, 2, 3) to the Stream.of() method.
Arrays.stream()
Arrays.stream() method creates a stream instance from an existing array. The resulting stream instance will have all the elements of the array.
Syntax
Example
Output
Explanation
The stream instance is created from the values of the array passed to the Arrays.stream() method. The created stream instance will have all the values of the array.
Stream.concat()
Stream.concat methods combine two existing streams to produce a new stream. The resultant stream will have all the elements of the first stream followed by all the elements of the second stream.
Syntax
Example
Output
Explanation
In this example, stream3 is created by combining stream1 and stream2. stream3 contains all the elements of stream1 followed by all the elements of stream2.
Stream.generate()
Stream.generate() returns an infinite sequential unordered stream where the values are generated by the provided Supplier.
Let's understand the important terms.
Infinite The values in the stream are infinite (i.e.) unlimited.
Unordered Repeated execution of the stream might produce different results.
Supplier A functional interface in Java represents an operation that takes no argument and returns a result.
Stream.generate() is useful to create infinite values like random integers, UUIDs (Universally Unique Identifiers), constants, etc. Since the resultant stream is infinite, it can be limited using the limit() method to make it run infinite time.
Syntax
Example
Output
Explanation
- In this example, we created an infinite stream of UUIDs using the Stream.generate() method.
- The Supplier is UUID::randomUUID that generates random UUIDs.
- Since the stream is infinite, it is limited using the limit() method.
Stream.iterate()
Stream.iterate() returns a infinite sequential ordered stream stream where the values are generated by the provided UnaryOperator. Lets understand the important terms.
Infinite The values in the stream are infinite (i.e. unlimited).
Ordered Repeated execution of the stream produces the same results.
UnaryOperator A functional interface in Java that takes one argument and returns a result that is of the same type as its argument.
Stream.iterate() methods accept two arguments, an initial value called the seed value and a unary operator. The first element of the resulting stream will be the seed value. The following elements are created by applying the unary operator on the previous element.
Syntax
Example
Output
Explanation
- In this example, we created an infinite ordered stream using the Stream.iterate() method that generates multiples of 2.
- The first element of the stream is 1, which is the seed value passed. The following elements are created by applying the unary operator n -> n * 2 on the previous element.
- Since the stream is infinite, the results are limited using the limit() method.
Comparison Based Stream Operations
min and max
These terminal operations return the minimum and maximum element of a stream based on a specified comparator. For example:
distinct
The distinct operation filters out duplicate elements from a stream, based on their natural order or the provided comparator. For example:
allMatch, anyMatch, and noneMatch
These terminal operations check if certain conditions are true for all, any, or none of the elements in a stream. They return a boolean value indicating the result. For example:
In the above example, allMatch checks if all numbers are even (false), anyMatch checks if any number is even (true), and noneMatch checks if none of the numbers are negative (true).
Stream Short-circuit Operations
Stream short-circuit operations can be better understood with Java's logical && and || operators.
- expression1 && expression2 doesn't evaluate expression2 if expression1 is false because false && anything is always false.
- expression1 || expression2 doesn't evaluate expression2 if expression1 is true because true || anything is always true.
Stream short-circuit operations are those that can terminate without processing all the elements in a stream. Short-circuit operations can be classified into two types.
- Intermediate short-circuit operations
- Terminal short-circuit operations
Intermediate Short-Circuit Operations
Intermediate short-circuit operations produce a finite stream from an infinite stream. The intermediate short-circuit operation in the Java stream is limit().
Example
Output
Explanation
The method limit(3) reduced the stream size from 5 to 3.
Terminal Short-Circuit Operations
Terminal short-circuit operations are those that can produce the result before processing all the elements of the stream. The terminal short-circuit operations in stream are findFirst(), findAny(), allMatch(), anyMatch(), and noneMatch().
Example
Ouput
Explanation
The terminal operation anyMatch(x -> x == 3) halts once 3 is found and doesn't process the remaining stream elements.
Parallel Stream
By default, all stream operations are sequential in Java unless explicitly specified as parallel. Parallel streams are created in Java in two ways.
- Calling the parallel() method on a sequential stream.
- Calling parallelStream() method on a collection.
Parallel streams are useful when we have many independent tasks that can be processed simultaneously to minimize the running time of the program.
parallel()
parallel() method is called on the existing sequential stream to make it parallel.
Example
Output
Explanation
The stream instance is converted to parallel stream by calling stream.parallel(). Since the forEach() method is called on a parallel stream, the output order will not be same as the input order because the elements are processed parallel.
parallelStream()
parallelStream() is called on Java collections like List, Set, etc to make it a parallel stream in Java.
Example
Output
Explanation
The list object is converted to a parallel stream by calling the parallelStream() method.
Method Types and Pipelines
-
Intermediate operations transform or filter elements in a stream, returning a new stream. Examples: filter, map, distinct, sorted, limit.
-
Terminal operations produce a result or side effect, marking the end of a stream. Examples: forEach, collect, reduce, count, min, max, anyMatch, allMatch, noneMatch.
-
Short-circuiting operations can terminate stream processing early based on a condition. Examples: findFirst, findAny, limit.
-
Pipelines chain intermediate and terminal operations, processing data in a fluent and expressive manner. Each operation takes input from the previous and produces output for the next. Pipelines allow concise and declarative coding. Result is obtained from the terminal operation.
Example of using method types and pipeline in streams:
In this example, the stream pipeline starts with the stream() method on the numbers list. It then applies the filter intermediate operation to keep only the even numbers, followed by the map operation to double each number. Finally, the reduce terminal operation sums up all the elements in the stream. The output is the sum of the doubled even numbers, which is 12.
Lazy Evaluation
Lazy evaluation (aka) call-by-need evaluation is an evaluation strategy that delays evaluating an expression until its value is needed. All intermediate operations are performed on the stream only when a terminal operation is invoked on it. Lazy evaluation is one of the critical characteristics of Java streams that allows significant optimizations.
Example
Consider the below example where we created a stream that filters odd numbers.
Output
Explanation
-
Each element of the above stream is passed to the filter() method where odd numbers are filtered and then the peek() method where the filtered value is printed.
-
Finally, we print the text Result and the filtered values.
-
If we look at the statements, the ideal order of printing should be
- But we got a different order because the intermediate operations filter() and peek() are not executed until the terminal operation collect() is invoked on the stream.
Important Points to Remember About Java Streams
- Streams are not data structures but rather a sequence of elements that can be processed in a pipeline of operations.
- Streams support two types of operations: intermediate and terminal. Intermediate operations transform or filter the stream, while terminal operations produce a result or side effect.
- Streams are designed to be lazy, meaning that the elements are processed on-demand as the terminal operation is executed.
- It's important to close streams that are opened from I/O channels or resources by using the close() method or by utilizing try-with-resources to ensure proper resource management.
- Java streams are not suitable for all scenarios. In some cases, traditional iteration using loops may be more appropriate and efficient.
FAQs
FAQs about Java Streams:
-
How do I convert a Stream back to a collection or an array? Ans: To convert a Stream back to a collection, you can use the collect terminal operation and provide a collector such as toList() or toSet(). If you need an array, you can use toArray() method after collecting the stream.
-
Can I reuse a Stream once it has been operated upon? Ans: No, once a Stream has been operated upon (either by an intermediate or a terminal operation), it cannot be reused. If you need to perform multiple operations, you can create a new Stream from the original data source.
-
How can I handle exceptions within a stream operation? Ans: Exceptions within a stream operation can be handled using the try-catch block inside the lambda expression or by using the exceptionally method with CompletableFuture if you are working with asynchronous streams.
-
What is the difference between findFirst and findAny methods in streams? Ans: Both findFirst and findAny methods return an Optional that may contain the first element of the stream. However, findFirst returns the first element encountered in a sequential stream, whereas findAny returns any element, making it more suitable for parallel streams.
:::section{.
summary}
Conclusion
- Streams in Java provide a sequence of objects that support various sequential and parallel aggregate operations.
- The Stream API in Java offers both intermediate and terminal operations that can be applied to stream instances.
- Intermediate operations, such as filter, map, and sorted, return a new stream and are lazily evaluated, meaning they are not executed until a terminal operation is invoked.
- Terminal operations, including forEach, collect, min, and max, produce the final results of the stream.
- Some stream operations are short-circuiting, allowing early termination without processing all elements in the stream.
- Lazy evaluation is a fundamental characteristic of streams in Java, ensuring that intermediate operations are performed only when a terminal operation is invoked.
:::