Skip to main content

Ch 14.2 Intermediate Operations

Intermediate operations form the stream pipeline and specify which processing logic to apply. The result of an intermediate operation always returns a Stream, so multiple intermediate operations can be chained together.

Intermediate operations are lazy— they are not actually executed until a terminal operation is called.

1. filter() — Conditional Filtering​

Takes a Predicate<T> and passes only elements where the condition is true.

import java.util.*;
import java.util.stream.*;

public class FilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Keep only even numbers
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.print(n + " ")); // 2 4 6 8 10
System.out.println();

// Combining multiple conditions (&&, ||, !)
List<String> words = Arrays.asList("Java", "Python", "Go", "JavaScript", "Kotlin", "Rust");
words.stream()
.filter(w -> w.length() >= 4) // at least 4 characters
.filter(w -> w.contains("a")) // contains 'a'
.filter(w -> !w.startsWith("J")) // does not start with J
.forEach(System.out::println); // Python, Kotlin
}
}
// filter with object streams
record Product(String name, String category, int price, boolean inStock) {}

public class FilterObjectExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", "Electronics", 1_200_000, true),
new Product("Mouse", "Electronics", 35_000, true),
new Product("Keyboard", "Electronics", 80_000, false),
new Product("Desk", "Furniture", 250_000, true),
new Product("Chair", "Furniture", 180_000, true)
);

// In-stock electronics priced at or below 100,000
products.stream()
.filter(Product::inStock)
.filter(p -> p.category().equals("Electronics"))
.filter(p -> p.price() <= 100_000)
.forEach(p -> System.out.println(p.name() + ": " + p.price()));
// Mouse: 35000
}
}

2. map() — Transformation​

Takes a Function<T, R> and transforms each element into a different value.

import java.util.*;
import java.util.stream.*;

public class MapExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");

// Convert strings to uppercase
words.stream()
.map(String::toUpperCase)
.forEach(System.out::println); // APPLE, BANANA, CHERRY

// String to length (type conversion)
words.stream()
.map(String::length)
.forEach(n -> System.out.print(n + " ")); // 5 6 6
System.out.println();

// Extract first character
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.map(name -> name.charAt(0))
.forEach(c -> System.out.print(c + " ")); // A B C
System.out.println();

// Square each number
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.map(n -> n * n)
.forEach(n -> System.out.print(n + " ")); // 1 4 9 16 25
System.out.println();
}
}

mapToInt, mapToDouble, mapToLong​

Converts an object stream to a primitive stream, enabling statistical methods like sum() and average().

record Employee(String name, double salary) {}

public class MapToIntExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", 5_500_000),
new Employee("Bob", 4_200_000),
new Employee("Charlie", 6_800_000)
);

// Stream<Employee> -> DoubleStream
double totalSalary = employees.stream()
.mapToDouble(Employee::salary)
.sum();
System.out.printf("Total salary: %,.0f%n", totalSalary);

double avgSalary = employees.stream()
.mapToDouble(Employee::salary)
.average()
.orElse(0.0);
System.out.printf("Average salary: %,.0f%n", avgSalary);
}
}

3. flatMap() — Flattening Nested Collections​

Flattens nested collection structures into a single stream.

import java.util.*;
import java.util.stream.*;

public class FlatMapExample {
public static void main(String[] args) {
// Flatten a list of lists into a single stream
List<List<Integer>> nestedNumbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6, 7, 8, 9)
);

nestedNumbers.stream()
.flatMap(Collection::stream) // convert each list to stream and merge
.forEach(n -> System.out.print(n + " ")); // 1 2 3 4 5 6 7 8 9
System.out.println();

// Split strings into individual words
List<String> sentences = Arrays.asList(
"Hello World",
"Java Stream API",
"FlatMap Example"
);

sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(word -> System.out.print(word + " "));
System.out.println();
}
}
// map vs flatMap comparison
public class MapVsFlatMap {
public static void main(String[] args) {
List<String> words = Arrays.asList("Hello", "World");

// map: produces Stream<Stream<String>> (undesired result)
Stream<Stream<String>> mapResult = words.stream()
.map(word -> Arrays.stream(word.split("")));
System.out.println("map result type: Stream<Stream<String>>");

// flatMap: produces Stream<String> (desired result - all chars in one stream)
words.stream()
.flatMap(word -> Arrays.stream(word.split("")))
.distinct()
.sorted()
.forEach(c -> System.out.print(c + " ")); // H W d e l o r
System.out.println();
}
}

4. distinct() — Removing Duplicates​

public class DistinctExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5, 1);

numbers.stream()
.distinct()
.forEach(n -> System.out.print(n + " ")); // 1 2 3 4 5
System.out.println();

// For objects, equals() and hashCode() must be correctly implemented.
// Records automatically implement equals/hashCode.
record Point(int x, int y) {}

List<Point> points = Arrays.asList(
new Point(1, 2), new Point(3, 4), new Point(1, 2), new Point(5, 6)
);

points.stream()
.distinct()
.forEach(p -> System.out.print(p + " ")); // Point[1,2] Point[3,4] Point[5,6]
System.out.println();
}
}

5. sorted() — Sorting​

import java.util.*;
import java.util.stream.*;

public class SortedExample {
public static void main(String[] args) {
// Natural ordering (requires Comparable implementation)
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 9, 2, 7);
numbers.stream()
.sorted()
.forEach(n -> System.out.print(n + " ")); // 1 2 3 5 7 8 9
System.out.println();

// Reverse ordering
numbers.stream()
.sorted(Comparator.reverseOrder())
.forEach(n -> System.out.print(n + " ")); // 9 8 7 5 3 2 1
System.out.println();

// Object sorting with Comparator
record Person(String name, int age) {}

List<Person> people = Arrays.asList(
new Person("Charlie", 25),
new Person("Alice", 30),
new Person("Bob", 20),
new Person("David", 25)
);

// Sort by age ascending
people.stream()
.sorted(Comparator.comparingInt(Person::age))
.forEach(p -> System.out.println(p.name() + ": " + p.age()));
System.out.println();

// Sort by age ascending, then by name ascending (compound sort)
people.stream()
.sorted(Comparator.comparingInt(Person::age)
.thenComparing(Person::name))
.forEach(p -> System.out.println(p.name() + ": " + p.age()));
}
}

6. limit() and skip() — Size Limiting and Skipping​

public class LimitSkipExample {
public static void main(String[] args) {
// limit: take at most n elements
Stream.iterate(1, n -> n + 1)
.limit(5)
.forEach(n -> System.out.print(n + " ")); // 1 2 3 4 5
System.out.println();

// skip: skip the first n elements
IntStream.rangeClosed(1, 10)
.skip(3)
.forEach(n -> System.out.print(n + " ")); // 4 5 6 7 8 9 10
System.out.println();

// Implementing pagination (3 items per page, page 2)
List<String> items = Arrays.asList("A", "B", "C", "D", "E", "F", "G", "H", "I");
int pageSize = 3;
int pageNum = 2; // 0-based

List<String> page = items.stream()
.skip((long) pageNum * pageSize) // skip previous pages
.limit(pageSize) // take current page size
.collect(Collectors.toList());
System.out.println("Page 2: " + page); // [G, H, I]
}
}

7. peek() — Intermediate Debugging​

Similar to forEach but returns the stream, so it can be inserted in the pipeline for debugging.

public class PeekExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

List<Integer> result = numbers.stream()
.peek(n -> System.out.println("original: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println(" passed filter: " + n))
.map(n -> n * 3)
.peek(n -> System.out.println(" after map: " + n))
.collect(Collectors.toList());

System.out.println("Final result: " + result);
}
}
warning

Use peek() for debugging purposes only. Placing code with side effects inside peek() can cause unpredictable behavior in parallel streams.

8. Performance Impact of Operation Order​

The order of intermediate operations affects performance.

import java.util.*;
import java.util.stream.*;

public class OperationOrderExample {
static int filterCount = 0;
static int mapCount = 0;

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Inefficient order: map first -> 10 map calls, then filter
filterCount = 0; mapCount = 0;
numbers.stream()
.map(n -> { mapCount++; return n * 2; })
.filter(n -> { filterCount++; return n > 10; })
.collect(Collectors.toList());
System.out.println("map first - map calls: " + mapCount + ", filter calls: " + filterCount);
// map: 10, filter: 10

// Efficient order: filter first -> only passing elements go through map
filterCount = 0; mapCount = 0;
numbers.stream()
.filter(n -> { filterCount++; return n > 5; })
.map(n -> { mapCount++; return n * 2; })
.collect(Collectors.toList());
System.out.println("filter first - filter calls: " + filterCount + ", map calls: " + mapCount);
// filter: 10, map: 5 (only half the elements go through map)
}
}
tip

Performance optimization principles:

  1. Place filter early in the pipeline to reduce the number of elements processed.
  2. Defer expensive operations (map, sort) as late as possible.
  3. Combining limit with findFirst/findAny uses short-circuit evaluation to avoid processing all elements.

9. Practical Example: Product Pipeline​

import java.util.*;
import java.util.stream.*;

record Product(String name, String category, int price, double rating, boolean inStock) {}

public class ProductPipelineExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop Pro", "Electronics", 1_800_000, 4.8, true),
new Product("Wireless Mouse", "Electronics", 45_000, 4.5, true),
new Product("USB Hub", "Electronics", 28_000, 4.2, false),
new Product("Ergonomic Chair", "Furniture", 450_000, 4.9, true),
new Product("Standing Desk", "Furniture", 680_000, 4.7, true),
new Product("27-inch Monitor", "Electronics", 520_000, 4.6, true),
new Product("Keyboard", "Electronics", 150_000, 4.3, true),
new Product("Webcam", "Electronics", 85_000, 4.1, false)
);

System.out.println("=== In-stock Electronics (100k-2M, Rating 4.4+, by price) ===");
products.stream()
.filter(Product::inStock)
.filter(p -> p.category().equals("Electronics"))
.filter(p -> p.price() >= 100_000 && p.price() <= 2_000_000)
.filter(p -> p.rating() >= 4.4)
.sorted(Comparator.comparingInt(Product::price))
.map(p -> String.format("%-20s | %,8d | %.1f stars",
p.name(), p.price(), p.rating()))
.forEach(System.out::println);

System.out.println("\n=== Product names by category ===");
Map<String, List<String>> byCategory = products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.mapping(Product::name, Collectors.toList())
));
byCategory.forEach((cat, names) ->
System.out.println(cat + ": " + names));

System.out.println("\n=== Top 3 most expensive products ===");
products.stream()
.sorted(Comparator.comparingInt(Product::price).reversed())
.limit(3)
.map(Product::name)
.forEach(System.out::println);
}
}