Module: Stream API

Intermediate Operations

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 names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

List longNames = names.stream() .filter(name -> name.length() > 4) // Keep names longer than 4 characters .collect(Collectors.toList());

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 n elements 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]