Java Stream Intermediate Operations: filter, map, sorted and More

0

Intermediate Operations

Intermediate operations are the building blocks that shape a Java Stream pipeline. They allow you to filter elements, transform values, sort data, remove duplicates, and perform many other forms of processing before the final result is produced.


The most important characteristic of an intermediate operation is that it does not normally finish the stream pipeline. Instead, it returns another stream, allowing multiple operations to be connected together.

What Is an Intermediate Operation?

An intermediate operation takes the current stream and returns a new stream that represents another stage of processing. This makes it possible to build readable pipelines where each operation performs one focused task.

import java.util.List;

public class IntermediateExample {
    public static void main(String[] args) {

        List<String> names = List.of(
            "Amit",
            "Anita",
            "Rahul",
            "Arjun"
        );

        names.stream()
             .filter(name -> name.startsWith("A"))
             .map(String::toUpperCase)
             .forEach(System.out::println);
    }
}

The filter() operation keeps names beginning with A. The map() operation converts those names to uppercase. Both are intermediate operations because they continue the stream pipeline rather than consuming it.

Important: Intermediate operations are generally lazy. They describe the processing that should happen, but the actual pipeline processing normally begins only when a terminal operation is invoked.

Common Intermediate Operations

Operation Purpose Returns
filter() Keeps elements that satisfy a condition Stream
map() Transforms each element Stream
flatMap() Flattens nested streams Stream
sorted() Orders stream elements Stream
distinct() Removes duplicate elements Stream
limit() Keeps only a specified number of elements Stream
skip() Ignores the first specified elements Stream
peek() Allows observation of elements during processing Stream

Intermediate Operations Are Lazy

Consider the following code:

import java.util.List;

public class LazyOperation {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(10, 20, 30);

        numbers.stream()
               .filter(number -> {
                   System.out.println("Checking " + number);
                   return number > 10;
               });

        System.out.println("Finished");
    }
}

You might expect the filtering code to print something. It does not. The intermediate operation has only built part of the pipeline. Nothing consumes the stream.


Add a terminal operation:

numbers.stream()
       .filter(number -> {
           System.out.println("Checking " + number);
           return number > 10;
       })
       .forEach(System.out::println);

Now the stream is consumed, so the intermediate operation actually participates in processing.

Remember: Think of intermediate operations as instructions written on a processing pipeline. The instructions describe the work; a terminal operation starts the actual journey through the pipeline.

filter() as an Intermediate Operation

The filter() operation keeps only elements that satisfy a given condition. It is one of the most frequently used intermediate operations.

import java.util.List;

public class FilterExample {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            5, 12, 18, 21, 30
        );

        numbers.stream()
               .filter(number -> number > 15)
               .forEach(System.out::println);
    }
}

Only values greater than 15 continue through the pipeline. The original list is not changed.

map() as an Intermediate Operation

The map() operation transforms each element into another value.

import java.util.List;

public class MapExample {
    public static void main(String[] args) {

        List<String> names = List.of(
            "Amit",
            "Priya",
            "Rahul"
        );

        names.stream()
             .map(String::toUpperCase)
             .forEach(System.out::println);
    }
}

Each string is transformed into its uppercase representation. The number of elements normally remains the same, although the values themselves change.

Combining Intermediate Operations

The real strength of intermediate operations appears when they are combined into a pipeline.

import java.util.List;

public class PipelineExample {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            5, 12, 18, 25, 30, 41
        );

        numbers.stream()
               .filter(number -> number > 10)
               .map(number -> number * 2)
               .forEach(System.out::println);
    }
}

The pipeline first removes values that are not greater than 10. The remaining values are then doubled. Finally, the terminal operation prints the results.

This reads almost like a sentence: take the numbers, keep those above 10, double them, and process the results. That expressive style is one of the major reasons streams are popular in modern Java code.

Intermediate Operations Can Change the Number of Elements

Not every intermediate operation preserves the number of elements. This is an important distinction when designing a pipeline.

Operation Effect on Element Count
filter() Can reduce the number of elements
map() Normally preserves the number of elements
flatMap() Can increase, reduce, or preserve the number of elements
distinct() Can reduce the number of elements
limit() Can reduce the number of elements
skip() Can reduce the number of elements
sorted() Normally preserves the number of elements

Ordering Intermediate Operations

The order of intermediate operations can affect both the meaning and performance of a pipeline. If an inexpensive filtering condition can remove many elements, placing it earlier can reduce the amount of work performed by later operations.

numbers.stream()
       .filter(number -> number > 100)
       .map(number -> expensiveTransformation(number))
       .forEach(System.out::println);

Here, the filter runs before the transformation. If most numbers are rejected, the potentially expensive transformation runs for fewer elements.

Industry Tip: When several operations are possible, place selective and inexpensive filtering operations early when doing so preserves the required behavior. This can reduce unnecessary processing.

Short-Circuiting Intermediate Operations

Some stream operations can limit how much data needs to flow through the pipeline. For example, limit() can stop processing after the required number of elements has been obtained.

import java.util.stream.Stream;

public class LimitExample {
    public static void main(String[] args) {

        Stream.iterate(1, number -> number + 1)
              .limit(5)
              .forEach(System.out::println);
    }
}

The generated stream could continue indefinitely, but limit(5) restricts the pipeline to five elements.

Stateful and Stateless Intermediate Operations

Intermediate operations can also be considered in terms of whether processing one element requires information about other elements.

Type Examples General Idea
Stateless filter(), map() Each element can generally be processed without remembering previous elements
Stateful sorted(), distinct() Processing may require information about multiple elements

This distinction becomes particularly important when discussing stream performance and parallel processing. Operations such as sorting may require more coordination than straightforward filtering or mapping.

Intermediate Operations Do Not Usually Modify the Source

A stream pipeline does not automatically mutate the collection from which the stream was created.

import java.util.List;

public class SourceUnchanged {
    public static void main(String[] args) {

        List<String> names = List.of(
            "Amit",
            "Priya",
            "Rahul"
        );

        names.stream()
             .map(String::toUpperCase)
             .forEach(System.out::println);

        System.out.println(names);
    }
}

The stream produces uppercase values, but the original list still contains the original strings.

A Practical Employee Example

Imagine an application that stores employee records. You want to select employees with salaries above a threshold and then extract their names.

import java.util.List;

public class EmployeePipeline {

    record Employee(String name, double salary) {}

    public static void main(String[] args) {

        List<Employee> employees = List.of(
            new Employee("Amit", 45000),
            new Employee("Priya", 72000),
            new Employee("Rahul", 58000),
            new Employee("Neha", 85000)
        );

        employees.stream()
                 .filter(employee -> employee.salary() > 60000)
                 .map(Employee::name)
                 .forEach(System.out::println);
    }
}

The first intermediate operation selects employees whose salary exceeds 60000. The second transforms each employee object into a name. Notice how the pipeline changes the type of data flowing through it: the source contains Employee objects, while the later stage contains strings.

Intermediate Operations vs Terminal Operations

Intermediate Operation Terminal Operation
Returns another stream Produces a final result or action
Normally lazy Triggers stream processing
Can be chained Normally ends the pipeline
Examples: filter(), map(), sorted() Examples: collect(), count(), forEach()

Common Beginner Mistakes

  • Expecting immediate execution: Intermediate operations are generally lazy and need a terminal operation to trigger processing.
  • Trying to use an intermediate operation as the final result: Calling filter() alone does not normally produce the final collection or value you want.
  • Assuming every operation preserves element count: Operations such as filter() and distinct() can remove elements.
  • Ignoring operation order: A poorly ordered pipeline may perform expensive work on elements that could have been rejected earlier.
  • Mutating external state inside lambdas: Side effects can make stream pipelines harder to reason about, especially when parallel streams are involved.
  • Making pipelines unnecessarily long: A stream should improve clarity, not turn a simple operation into an unreadable chain.

Best Practices

  • Give each intermediate operation a clear purpose.
  • Use filtering early when it can safely reduce the amount of later processing.
  • Prefer stateless operations when possible.
  • Avoid unnecessary side effects inside stream pipelines.
  • Keep complex business logic in well-named methods when lambdas become difficult to read.
  • Use intermediate operations to express transformations clearly rather than forcing every operation into a stream.

Interview Insights

Question: What is an intermediate operation in Java Streams?

Answer: It is an operation that processes a stream and returns another stream, allowing additional operations to be chained. Intermediate operations are generally lazy.

Question: Why are intermediate operations called lazy?

Answer: They normally do not process elements when the pipeline is being constructed. Processing begins when a terminal operation consumes the stream.

Question: Can multiple intermediate operations be chained?

Answer: Yes. Operations such as filter(), map(), sorted(), and distinct() can be combined into a pipeline before a terminal operation.

Quick Revision

Concept Key Point
Intermediate operation Returns another stream and can be chained
Lazy evaluation Processing generally waits for a terminal operation
filter() Removes elements that do not satisfy a condition
map() Transforms each element
flatMap() Flattens nested structures into a single stream
sorted() Orders stream elements
distinct() Removes duplicates
limit() Restricts the number of elements flowing through the pipeline
Operation order Can affect both readability and performance

Final Takeaway

Intermediate operations are what give Java streams their pipeline-oriented style. They allow you to progressively shape data without immediately producing a final result. Once you understand that operations such as filter(), map(), flatMap(), sorted(), and distinct() are lazy, chainable steps, stream code becomes much easier to read and design. The next important step is learning how terminal operations finally consume these pipelines and turn processing into useful results.

Post a Comment

0Comments
Post a Comment (0)