Introduction
The Java Stream API is a powerful tool for processing collections of data in a declarative and efficient manner. Intermediate operations are the building blocks of a stream pipeline, transforming the stream without consuming it. They allow you to filter, map, and modify the elements of a stream before a terminal operation is applied. This document focuses on the common intermediate operations available in Java 8 and beyond.
Key Characteristics of Intermediate Operations
- Lazy Evaluation: Intermediate operations are lazy. They don't perform any actual processing until a terminal operation is invoked. This allows for optimization and avoids unnecessary computations.
- Return a Stream: Each intermediate operation returns a new stream, allowing for chaining of operations. The original stream remains unchanged.
- Stateless: Most intermediate operations are stateless, meaning they don't maintain any internal state between elements. (Exceptions exist, like
sorted()).
Common Intermediate Operations
Here's a breakdown of frequently used intermediate operations with examples:
1. filter(Predicate<T> predicate)
- Purpose: Selects elements from the stream that match the given predicate (a boolean-valued function).
- Example:
```java
List
List
System.out.println(longNames); // Output: [Alice, Charlie, David]
2. map(Function<T, R> mapper)
- Purpose: Transforms each element of the stream into a new element using the provided mapper function.
- Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> numberStrings = numbers.stream()
.map(number -> "Number: " + number) // Convert each number to a string
.collect(Collectors.toList());
System.out.println(numberStrings); // Output: [Number: 1, Number: 2, Number: 3, Number: 4, Number: 5]
3. flatMap(Function<T, Stream<R>> mapper)
- Purpose: Transforms each element of the stream into a stream of new elements, then flattens all the resulting streams into a single stream. Useful for handling nested collections.
- Example:
List<List<Integer>> listOfLists = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4, 5),
Arrays.asList(6)
);
List<Integer> flattenedList = listOfLists.stream()
.flatMap(List::stream) // Flatten the list of lists into a single stream
.collect(Collectors.toList());
System.out.println(flattenedList); // Output: [1, 2, 3, 4, 5, 6]
4. distinct()
- Purpose: Returns a stream consisting of the distinct elements of the original stream. Uses
equals()method for comparison. - Example:
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
.distinct() // Remove duplicate elements
.collect(Collectors.toList());
System.out.println(distinctNumbers); // Output: [1, 2, 3, 4, 5]
5. sorted()
- Purpose: Sorts the elements of the stream in natural order (if the elements implement
Comparable). - Example:
List<String> names = Arrays.asList("Charlie", "Alice", "Bob", "David");
List<String> sortedNames = names.stream()
.sorted() // Sort alphabetically
.collect(Collectors.toList());
System.out.println(sortedNames); // Output: [Alice, Bob, Charlie, David]
sorted(Comparator<T> comparator): Allows you to provide a custom comparator for sorting.
List<String> names = Arrays.asList("Charlie", "Alice", "Bob", "David");
List<String> sortedByLength = names.stream()
.sorted(Comparator.comparingInt(String::length)) // Sort by string length
.collect(Collectors.toList());
System.out.println(sortedByLength); // Output: [Bob, Alice, David, Charlie]
6. peek(Consumer<T> action)
- Purpose: Performs an action on each element of the stream without modifying the stream itself. Useful for debugging or side effects.
- Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> processedNumbers = numbers.stream()
.peek(number -> System.out.println("Processing: " + number)) // Print each number
.map(number -> number * 2) // Double each number
.collect(Collectors.toList());
System.out.println(processedNumbers); // Output: [2, 4, 6, 8, 10] (with "Processing: ..." printed for each number)
7. limit(long maxSize)
- Purpose: Limits the stream to a specified maximum size.
- Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> firstThree = numbers.stream()
.limit(3) // Take only the first 3 elements
.collect(Collectors.toList());
System.out.println(firstThree); // Output: [1, 2, 3]
8. skip(long n)
- Purpose: Skips the first
nelements of the stream. - Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> skipFirstTwo = numbers.stream()
.skip(2) // Skip the first 2 elements
.collect(Collectors.toList());
System.out.println(skipFirstTwo); // Output: [3, 4, 5, 6, 7, 8, 9, 10]
Chaining Intermediate Operations
The real power of the Stream API comes from chaining multiple intermediate operations together.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
List<String> result = names.stream()
.filter(name -> name.startsWith("A")) // Filter names starting with "A"
.map(String::toUpperCase) // Convert to uppercase
.sorted() // Sort alphabetically
.collect(Collectors.toList());
System.out.println(result); // Output: [ALICE]