Java Streams API (Filter, Map, Reduce)
The Streams API introduced in Java 8 provides a modern, functional-style approach to processing sequences of elements, such as collections. The Stream API allows you to perform operations on data in a clean, concise, and efficient way, enabling parallel processing and better readability.
Streams support operations like filtering, mapping, and reducing that make it easier to perform complex data manipulations in a more declarative way compared to traditional loops.
In this explanation, we will focus on the three main operations of the Streams API: Filter, Map, and Reduce.
1. Stream Overview
A Stream is a sequence of elements that can be processed in parallel or sequentially. The operations on streams can be classified into:
- Intermediate operations: These return a new stream (e.g.,
filter()
,map()
) and are lazy. They don’t execute until a terminal operation is invoked. - Terminal operations: These produce a result or a side-effect (e.g.,
collect()
,reduce()
,forEach()
) and trigger the processing of the stream.
2. Filter Operation
The filter()
operation is an intermediate operation that allows you to filter out elements based on a predicate (a condition). It returns a stream that only contains elements that satisfy the predicate.
Syntax:
Stream<T> filter(Predicate<? super T> predicate)
- Predicate: A functional interface that takes an object and returns a boolean (true if the element should be included).
Example:
import java.util.*;
import java.util.stream.*;
public class FilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // Output: [2, 4, 6, 8]
}
}
In this example:
- We use
filter()
to select only even numbers from the list. collect(Collectors.toList())
is a terminal operation that collects the results into a new list.
3. Map Operation
The map()
operation is an intermediate operation that transforms each element of the stream into another form. It applies a given function to each element and returns a stream of the transformed elements.
Syntax:
Stream<R> map(Function<? super T, ? extends R> mapper)
- Function: A functional interface that takes an element of type
T
and returns an element of typeR
.
Example:
import java.util.*;
import java.util.stream.*;
public class MapExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "streams");
List<String> upperCaseWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseWords); // Output: [HELLO, WORLD, JAVA, STREAMS]
}
}
In this example:
- We use
map()
to convert all words in the list to uppercase. - The
String::toUpperCase
method reference is applied to each string element.
4. Reduce Operation
The reduce()
operation is a terminal operation that combines elements of the stream into a single result. It takes a binary operator that performs an accumulation (or reduction) of elements. It is commonly used for operations like summing, multiplying, or concatenating.
Syntax:
T reduce(T identity, BinaryOperator<T> accumulator)
- identity: The initial value (or identity value).
- accumulator: A function that takes two elements (the result of the previous step and the current element) and combines them into a single result.
Example: Sum of all elements in a list
import java.util.*;
import java.util.stream.*;
public class ReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // Identity is 0, accumulator is a + b
System.out.println(sum); // Output: 15
}
}
In this example:
- We use
reduce()
to sum up all the elements of the list. - The identity value is
0
, and the accumulator function is(a, b) -> a + b
.
Example: Concatenating strings
import java.util.*;
import java.util.stream.*;
public class ReduceStringExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("Java", "Streams", "Are", "Powerful");
String result = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(result); // Output: "Java Streams Are Powerful"
}
}
In this example:
- We use
reduce()
to concatenate all strings in the list into one string, with spaces in between.
5. Combining Filter, Map, and Reduce
You can easily combine filter()
, map()
, and reduce()
to perform more complex operations on streams.
Example: Sum of squares of even numbers
import java.util.*;
import java.util.stream.*;
public class CombinedExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
int sumOfSquares = numbers.stream()
.filter(n -> n % 2 == 0) // Filter even numbers
.map(n -> n * n) // Map to squares
.reduce(0, (a, b) -> a + b); // Reduce to sum
System.out.println(sumOfSquares); // Output: 120 (4 + 16 + 36 + 64)
}
}
In this example:
- We first filter out odd numbers, then map each remaining even number to its square, and finally reduce them to compute the sum.
6. Parallel Streams
The Streams API also supports parallel processing to take advantage of multi-core processors. You can convert a stream to a parallel stream using the parallelStream()
method or the parallel()
method on an existing stream.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
int sum = numbers.parallelStream()
.reduce(0, (a, b) -> a + b);
Using parallel streams can improve performance when dealing with large datasets, but it is important to note that parallelism may not always yield better performance due to overhead, especially for smaller datasets.
Key Points:
- filter(): Filters elements based on a condition.
- map(): Transforms elements to another form.
- reduce(): Combines elements into a single result.
- Stream operations can be chained together.
- Streams can be processed in parallel for better performance.