The Streams API, introduced in Java 8, revolutionized how developers work with collections by offering a declarative, functional-style approach to data processing. Rather than relying on loops and external iteration, streams enable concise and readable code that focuses on what to do with data rather than how to do it.
A stream is a sequence of elements that supports aggregate operations such as filtering, mapping, and reducing. Streams don’t store data themselves; instead, they operate on data sources like Collection
, List
, or arrays. Think of a stream as a pipeline through which data flows, undergoing transformations or actions along the way.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println); // Output: Alice
In this example, the stream filters names starting with "A" and prints them. This approach is both readable and expressive, eliminating boilerplate loop constructs.
Declarative and Functional: Streams favor a high-level, declarative style. Operations like filter
, map
, and collect
describe what should happen, not how to loop or manage state.
Lazy Evaluation: Intermediate operations (like filter
or map
) are lazy—they don't execute until a terminal operation (like forEach
, collect
, or reduce
) is invoked. This enables performance optimizations, such as short-circuiting.
Single Use: Streams can only be traversed once. After a terminal operation, the stream is considered consumed.
Can Be Parallelized: With .parallelStream()
, the same operations can be processed in parallel, improving performance for large datasets.
Feature | Collection | Stream |
---|---|---|
Data Structure | Stores elements (in memory) | Computes elements on demand |
Iteration | External (e.g., for-loop) | Internal (handled by stream pipeline) |
Mutability | Can be modified (add/remove) | Immutable – does not modify source |
Reusability | Can iterate multiple times | Consumed after one use |
Streams simplify complex data manipulations, such as filtering or aggregating, into a series of concise steps:
int sum = numbers.stream()
.filter(n -> n > 0)
.mapToInt(Integer::intValue)
.sum(); // Sum of positive integers
This avoids verbose loops and improves clarity, especially in multi-step transformations.
By separating the intent of data processing from the mechanics, the Streams API encourages cleaner, more maintainable code. As we delve deeper into filtering, mapping, reducing, and collecting in later sections, you’ll see just how powerful and elegant stream-based programming can be.
The Streams API in Java provides a powerful set of core operations that allow us to process collections in a fluent and expressive manner. The three most essential operations are filtering, mapping, and reducing. These form the backbone of functional-style data processing in Java.
filter()
is an intermediate operation that lets you retain elements based on a condition. It takes a predicate (a function that returns true
or false
) and produces a new stream with only the elements that match.
Example:
List<String> names = Arrays.asList("Alice", "Bob", "Amanda", "Brian");
List<String> filtered = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(filtered); // Output: [Alice, Amanda]
In this example, only names starting with "A" are selected.
map()
is another intermediate operation that transforms each element of the stream into another form. It takes a function and applies it to each element.
Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(nameLengths); // Output: [5, 3, 7]
Here, the map()
function transforms each string into its length.
reduce()
is a terminal operation that combines elements into a single result, such as a sum or concatenation. It takes a binary operator (e.g., addition) and applies it cumulatively.
Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // Output: 15
The stream reduces the list of integers to their total sum.
One of the most powerful aspects of streams is the ability to chain operations together to build complex data transformations in a clean and readable way.
Example:
List<String> words = Arrays.asList("stream", "filter", "map", "reduce", "collect");
int totalChars = words.stream()
.filter(word -> word.length() > 4) // Keep words longer than 4 characters
.map(String::length) // Convert to word length
.reduce(0, Integer::sum); // Sum the lengths
System.out.println(totalChars); // Output: 30
This example filters out short words, maps the remaining words to their lengths, and then sums those lengths.
import java.util.*;
import java.util.stream.Collectors;
public class StreamOperationsDemo {
public static void main(String[] args) {
// FILTER: Select names starting with "A"
List<String> names = Arrays.asList("Alice", "Bob", "Amanda", "Brian");
List<String> filtered = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println("Filtered (starts with A): " + filtered); // [Alice, Amanda]
// MAP: Transform names to their lengths
List<String> moreNames = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> nameLengths = moreNames.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("Name lengths: " + nameLengths); // [5, 3, 7]
// REDUCE: Sum a list of integers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum of numbers: " + sum); // 15
// CHAINING: Filter, map, and reduce
List<String> words = Arrays.asList("stream", "filter", "map", "reduce", "collect");
int totalChars = words.stream()
.filter(word -> word.length() > 4)
.map(String::length)
.reduce(0, Integer::sum);
System.out.println("Total characters (length > 4): " + totalChars); // 30
}
}
Operation | Purpose | Type |
---|---|---|
filter() |
Selects elements conditionally | Intermediate |
map() |
Transforms elements | Intermediate |
reduce() |
Combines elements into one value | Terminal |
Filtering, mapping, and reducing are foundational to stream processing. They allow developers to write clean, concise, and declarative code for complex data transformations. In the next section, we'll expand this foundation using Collectors
for grouped results and explore the benefits of parallel streams.
The Streams API provides a powerful way to process collections functionally, and collectors play a central role in gathering the results of stream operations. At the same time, parallel streams enable data processing across multiple threads for improved performance—if used wisely.
The Collectors
class provides a variety of static methods that can be used to collect stream elements into various formats. These methods are typically passed to the collect()
terminal operation.
Collectors.toList()
Gathers stream elements into a List
.
List<String> names = Stream.of("Alice", "Bob", "Charlie")
.collect(Collectors.toList());
Collectors.toSet()
Collects elements into a Set
, eliminating duplicates.
Set<Integer> numbers = Stream.of(1, 2, 2, 3)
.collect(Collectors.toSet()); // Output: [1, 2, 3]
Collectors.joining()
Concatenates strings into a single string, optionally with delimiters.
String result = Stream.of("Java", "Streams", "API")
.collect(Collectors.joining(", "));
System.out.println(result); // Output: Java, Streams, API
Collectors.groupingBy()
Groups elements by a classification function, returning a Map
.
List<String> words = Arrays.asList("apple", "banana", "apricot", "blueberry");
Map<Character, List<String>> grouped = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0)));
System.out.println(grouped);
// Output: {a=[apple, apricot], b=[banana, blueberry]}
Collectors.summarizingInt()
Provides summary statistics like count, sum, min, max, and average.
IntSummaryStatistics stats = Stream.of(3, 5, 7, 2, 9)
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats); // count=5, sum=26, min=2, average=5.2, max=9
Parallel streams offer a convenient way to leverage multi-core processors by dividing a stream’s elements and processing them concurrently.
To convert a regular stream into a parallel one, simply call parallel()
:
List<Integer> nums = IntStream.range(1, 10000)
.boxed()
.collect(Collectors.toList());
int sum = nums.parallelStream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
Collectors.toConcurrentMap()
or similar.forEachOrdered()
).List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
System.out.println("Using parallel forEach:");
letters.parallelStream().forEach(System.out::print); // Order not guaranteed
System.out.println("\nUsing parallel forEachOrdered:");
letters.parallelStream().forEachOrdered(System.out::print); // Order preserved
import java.util.*;
import java.util.function.Function;
import java.util.stream.*;
public class CollectorAndParallelDemo {
public static void main(String[] args) {
// toList()
List<String> names = Stream.of("Alice", "Bob", "Charlie")
.collect(Collectors.toList());
System.out.println("List: " + names);
// toSet()
Set<Integer> numbers = Stream.of(1, 2, 2, 3)
.collect(Collectors.toSet());
System.out.println("Set (no duplicates): " + numbers);
// joining()
String joined = Stream.of("Java", "Streams", "API")
.collect(Collectors.joining(", "));
System.out.println("Joined string: " + joined);
// groupingBy()
List<String> words = Arrays.asList("apple", "banana", "apricot", "blueberry");
Map<Character, List<String>> grouped = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0)));
System.out.println("Grouped by first letter: " + grouped);
// summarizingInt()
IntSummaryStatistics stats = Stream.of(3, 5, 7, 2, 9)
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println("Summary statistics: " + stats);
// Parallel stream sum
List<Integer> bigList = IntStream.range(1, 10_000)
.boxed()
.collect(Collectors.toList());
int sum = bigList.parallelStream()
.reduce(0, Integer::sum);
System.out.println("Parallel sum: " + sum);
// Demonstrating ordering issue
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
System.out.print("Parallel forEach: ");
letters.parallelStream().forEach(System.out::print); // May print out of order
System.out.print("\nParallel forEachOrdered: ");
letters.parallelStream().forEachOrdered(System.out::print); // Preserves order
}
}
Collector | Use Case |
---|---|
toList() , toSet() |
Gather into collections |
joining() |
Concatenate string results |
groupingBy() |
Classify elements into map groups |
summarizingInt() |
Collect numeric stats |
Parallel streams can greatly enhance performance but must be used with careful attention to side effects, ordering, and shared mutable state.
In the next section, we’ll explore more hands-on examples to bring together the concepts of filtering, mapping, reducing, and collecting—all in a functional style.
Streams provide a fluent, readable way to process collections with a sequence of operations—filtering, mapping, reducing, and collecting results. Below are runnable examples demonstrating these concepts, including parallel processing and collectors.
This example takes a list of integers, filters out odd numbers, squares the even ones, and sums the results.
import java.util.Arrays;
import java.util.List;
public class StreamExample1 {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sumOfSquares = numbers.stream()
// Filter: keep only even numbers
.filter(n -> n % 2 == 0)
// Map: square each remaining number
.map(n -> n * n)
// Reduce: sum all squared values
.reduce(0, Integer::sum);
System.out.println("Sum of squares of even numbers: " + sumOfSquares);
}
}
Explanation:
.filter()
narrows the stream to even numbers..map()
transforms each number to its square..reduce()
aggregates by summing all squared values. This declarative style replaces verbose loops and conditionals.Now we convert a stream of words to uppercase and group them by their first letter.
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamExample2 {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "avocado", "blueberry", "cherry");
Map<Character, List<String>> grouped = words.stream()
// Map to uppercase
.map(String::toUpperCase)
// Group by first character
.collect(Collectors.groupingBy(word -> word.charAt(0)));
System.out.println(grouped);
}
}
Explanation:
.map(String::toUpperCase)
transforms each string.Collectors.groupingBy()
collects elements into a map keyed by the first letter. This shows combining mapping with advanced collectors.Parallel streams can improve performance on large collections. This example calculates the sum of squares using parallel processing.
import java.util.List;
import java.util.stream.IntStream;
public class StreamExample3 {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.toList();
long start = System.currentTimeMillis();
int sumOfSquares = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.reduce(0, Integer::sum);
long end = System.currentTimeMillis();
System.out.println("Sum of squares (parallel): " + sumOfSquares);
System.out.println("Time taken (ms): " + (end - start));
}
}
Explanation:
.parallelStream()
processes the stream concurrently.These examples highlight the expressive power of streams:
Streams enable concise, readable, and maintainable code, making collection processing easier and often more efficient.