Stream Operations in Java
Introduction
The Java Streams API, introduced in Java 8, revolutionized how we process collections of data. A Stream pipeline consists of three parts: a source, zero or more intermediate operations, and a terminal operation.
Understanding the distinction between intermediate and terminal operations is crucial for writing efficient and bug-free Stream pipelines.
1. Intermediate Operations
Intermediate operations return a new Stream. They are lazy, meaning they are not executed until a terminal operation is invoked.
Common Intermediate Operations
filter(Predicate): Filters elements based on a condition.map(Function): Transforms each element into another object.flatMap(Function): Transforms each element into a stream of objects and flattens the result.sorted(): Sorts the elements (requires elements to beComparableor providing aComparator).distinct(): Removes duplicates.limit(long maxSize): Truncates the stream to a specific size.skip(long n): Discards the firstnelements.
Example:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class IntermediateExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
Stream<String> filteredStream = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted();
// Nothing has been printed or processed yet!
System.out.println("Stream created, but not executed.");
}
}
2. Terminal Operations
Terminal operations produce a non-stream result (like a List, Integer, or void). Once a terminal operation is executed, the stream is considered consumed and can no longer be used.
Common Terminal Operations
collect(Collector): Converts the stream into a collection (e.g.,List,Set,Map).forEach(Consumer): Iterates over each element (usually for side-effects like printing).reduce(BinaryOperator): Combines elements into a single value (e.g., sum, max).count(): Returns the number of elements.anyMatch(),allMatch(),noneMatch(): Returns a boolean based on a condition.findFirst(),findAny(): Returns anOptionalcontaining an element from the stream.
Example:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TerminalExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList()); // Terminal operation
System.out.println(result); // [ALICE, CHARLIE, DAVID]
}
}
3. Short-Circuiting Operations
Some operations don't need to process the entire stream to produce a result. These are known as short-circuiting operations. They are particularly useful for infinite streams.
- Intermediate Short-circuiting:
limit() - Terminal Short-circuiting:
anyMatch(),allMatch(),noneMatch(),findFirst(),findAny()
Example:
import java.util.stream.Stream;
public class ShortCircuitExample {
public static void main(String[] args) {
boolean hasEven = Stream.iterate(1, n -> n + 1) // Infinite stream 1, 2, 3...
.anyMatch(n -> n % 2 == 0); // Stops checking at 2
System.out.println("Contains even number? " + hasEven); // true
}
}
Summary
- Intermediate Operations (like
map,filter) return a Stream and are lazy. - Terminal Operations (like
collect,forEach) trigger execution and consume the stream. - Short-circuiting operations can optimize execution by terminating early once a condition is met.