Stream enables the concept of filtering where one method filters data and passes it to another method after processing. The intermediate operations transform a stream into another stream. Stream can only be used once, once a terminal operation (such as forEach
, collect
, or reduce
) is called on a stream, the stream is consumed and cannot be reused.
A stream is not a data structure but take input from the Collections, Arrays, and I/O channels. They provide methods to create streams (e.g. stream()
). Streams don’t change the original data structure. Instead, streams operate on the data source to produce a transformed or filtered result.
By calling the stream()
method on an appropriate data source, it will return a stream and you can perform a range of operations on the stream.
Terminal operations
For each
The forEach
method in Java is a convenient way to iterate over elements in a collection or other data structures. It is introduced as a part of the stream API and the Iterable
interface and is a terminal operation.
Warning
The semantics of
forEach()
is different when working on a stream than a collection. TheCollection.forEach()
uses the collection’s iterator (if one is specified), so the processing order of the items is defined. In contrast, the processing order ofCollection.stream().forEach()
is undefined.
default void forEach(Consumer<? super T> action)
@FunctionalInterface
public interface Consumer<T> { void accept(T t); }
The forEach()
method takes a Consumer type. The Consumer
is a functional interface that has a single method accept()
. This method performs an operation on the given input argument of type T
.
Consider the below example that iterates through a list, we implement the consumer interface through anonymous inner class.
public class Demo{
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
Consumer<Integer> con = new Consumer<Integer>() {
@Override
public void accept(Integer n) {
// In this example we just want to print each number
System.out.println(n);
}
};
// The forEach method will provide one value at a time to consumer object
nums.forEach(con);
}
}
3
4
2
7
5
Since the consumer interface is a functional interface, we can implement it using lambda expression.
public class Demo{
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
Consumer<Integer> con = n -> System.out.println(n);
nums.forEach(con);
}
}
We can further simply the code by passing the lambda expression as the forEach parameter.
public class Demo{
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
nums.forEach(n -> System.out.println(n));
}
}
We can use method reference to further simply this expression:
public class Demo{
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
nums.forEach(System.out::println);
}
}
Reduce
The reduce()
method is a terminal operation that combines the elements of a stream to create a single accumulated result by repeatedly applying a specified operation (e.g., summing numbers or concatenating strings).
With identity value
The reduce method can take a identity and a accumulator as parameter.
T reduce(T identity, BinaryOperator<T> accumulator);
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> { }
@FunctionalInterface
public interface BiFunction<T, U, R> { R apply(T t, U u); }
Identity
- Acts as a starting point and is also the default result for empty streams.
Accumulator
- The accumulator is a function that combines two elements of the same type (current result and next element) and produce a result of the same type. The
BinaryOperator
functional interface a subtype ofBiFunction
and has a methodapply()
that takes the two elements.
- The accumulator is a function that combines two elements of the same type (current result and next element) and produce a result of the same type. The
This example adds the numbers in the list together.
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// Create the stream
Stream<Integer> s1 = list.stream();
BinaryOperator<Integer> bo = new BinaryOperator<>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer+integer2;
}
};
// The identify value 0 serves as the starting point for the operation
// The initial value is set as 0, which means the accumulation starts at 0
int result = s1.reduce(0, bo);
System.out.println(result);
}
This is how the reduction work, for each element in the stream, the reduce()
method:
- Combines the current result (starting with the identity
0
) with the next stream element using theBinaryOperator
. - The process continues for all elements in the stream.
- Initial value =
0
- Step 1:
0 + 1 = 1
- Step 2:
1 + 2 = 3
- Step 3:
3 + 3 = 6
- Step 4:
6 + 4 = 10
- Step 5:
10 + 5 = 15
The final result is 15.
As discussed before, the entire process can be more succinct using the lambda expression.
public static void main(String[] args){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> s1 = list.stream();
BinaryOperator<Integer> bo = (integer, integer2) -> {
return integer+integer2;
};
int result = s1.reduce(0, bo);
System.out.println(result);
}
We can further simply it by passing the expression as argument.
public static void main(String[] args){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> s1 = list.stream();
int result = s1.reduce(0, (integer, integer2) -> integer + integer2);
System.out.println(result);
}
15
This is an example only, stream provides a terser way to sum the numbers by mapping stream into a IntStream
using mapToInt()
method, and the IntStream
provides a sum()
method for calculating the sum of a number stream.
Without identity value
The reduce()
method can be used without identity provided. When no identity value is provided, the method returns an Optional
because there may not always be a result (e.g., when the stream is empty). If the stream has one element, that element becomes the result.
Optional<T> reduce(BinaryOperator<T> accumulator);
When no identity is provided:
- The first element of the stream is treated as the initial value.
- The accumulator is applied to combine elements from the stream.
public static void main(String[] args){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> result = list.stream().reduce((a,b) -> a+b);
System.out.println(result.get());
}
15
Intermediate operations
Filter
The filter method is used to process elements of a stream and produce a new stream consisting of elements that satisfy a given predicate.
Stream<T> filter(Predicate<? super T> predicate);
@FunctionalInterface
public interface Predicate<T> { boolean test(T t); }
The Predicate
is also a functional interface that represents a condition (boolean
valued function). It takes one input and returns true
if the input satisfies the condition, false
otherwise. It works as described below.
- The filter method iterates over the elements of the stream.
- For each element, it evaluates the predicate condition.
- If the predicate returns
true
, the element is included in the resulting stream. - If the predicate returns
false
, the element is excluded.
public class Demo{
public static void main() {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
Stream<Integer> s1 = nums.stream();
Stream<Integer> s2 = s1.filter(n -> n%2==0);
}
}
Map
The map intermediate method transforms each element in a stream by applying a specific function.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
@FunctionalInterface
public interface Function<T, R> { R apply(T t); }
The Function
functional interface has a apply()
method to transform elements.
public class Demo{
public static void main() {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
Stream<Integer> s1 = nums.stream();
// This will double the value of each element in the stream
Stream<Integer> s2 = s1.map(n -> n*2);
}
}
In one expression
You can combine all stream operations in a single expression.
public class Demo{
public static void main() {
List<Integer> nums = Arrays.asList(3, 4, 2, 7 ,5);
int result = nums.stream()
.filter(n -> n%2 == 0)
.map(n -> n*2)
.reduce(0, (c,e) -> c+e);
}
}
Parallel stream
ParallelStream enables parallel processing of data. Unlike a normal stream (sequential stream), a parallel stream divides the data into multiple chunks and processes them concurrently using multiple threads.
It is useful for stream with independent values - the computation on one value in the stream does not depend on the results or processing of other values, such as filtering, doubling each element, summing up elements. It is not for performing on dependent values where the computation or processing of one value in the stream depends on the relative position or relationship with other elements, such as sorting.
You can create a parallel stream using the parallelStream()
method.
List<Integer> list = Arrays.asList(3, 4, 2, 7 ,5);
Stream<Integer> parallelStream = list.parallelStream();
Back to parent page: Java Standard Edition (Java SE) and Java Programming
Web_and_App_Development Programming_Languages Java Stream For_Each ParallelStream
Reference: