Skip to main content

Ch 14.4 Terminal Operations

Terminal operations are placed at the very end of a stream pipeline. They consume the stream's elements to produce a final result — such as a count, a collection, a single value, or just an execution of side effects. The moment a terminal operation is invoked, all intermediate operations in the pipeline execute, and the stream is closed.

1. forEach — Iteration and Output

Performs the specified action for each element. Commonly used for printing and debugging.

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

public class ForEachExample {
public static void main(String[] args) {
List<String> members = Arrays.asList("Alice", "Bob", "Charlie");

// Basic forEach
members.stream().forEach(System.out::println);

// With transformation in the pipeline
members.stream()
.map(String::toUpperCase)
.forEach(name -> System.out.println("Member: " + name));

// forEachOrdered: guarantees order (important for parallel streams)
members.parallelStream()
.forEachOrdered(System.out::println); // maintains original order
}
}
note

forEach is a terminal operation, not an intermediate one. Unlike peek(), it does not return a stream, so it must be the last call in the pipeline.

2. collect — Gathering Results

The most frequently used terminal operation. Collects stream results into a List, Set, Map, or a formatted string using Collectors.

Collecting to List, Set, and Map

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

public class CollectExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Avocado", "Blueberry");

// toList() - collect to List
List<String> list = fruits.stream()
.filter(f -> f.startsWith("A"))
.collect(Collectors.toList());
System.out.println("List: " + list); // [Apple, Avocado]

// Java 16+ shorthand
List<String> immutableList = fruits.stream()
.filter(f -> f.length() > 5)
.toList(); // unmodifiable
System.out.println("Immutable list: " + immutableList);

// toSet() - collect to Set (removes duplicates)
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 4);
Set<Integer> set = numbers.stream().collect(Collectors.toSet());
System.out.println("Set: " + set); // {1, 2, 3, 4} (order not guaranteed)

// toMap() - collect to Map
Map<String, Integer> nameLengthMap = fruits.stream()
.collect(Collectors.toMap(
f -> f, // key: the fruit name
String::length // value: the length
));
System.out.println("Map: " + nameLengthMap);
}
}

joining — Concatenate Strings

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

public class JoiningExample {
public static void main(String[] args) {
List<String> items = Arrays.asList("Apple", "Banana", "Cherry");

// Simple join
String joined = items.stream()
.collect(Collectors.joining());
System.out.println(joined); // AppleBananaCherry

// Join with delimiter
String withComma = items.stream()
.collect(Collectors.joining(", "));
System.out.println(withComma); // Apple, Banana, Cherry

// Join with delimiter, prefix, and suffix
String withBrackets = items.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(withBrackets); // [Apple, Banana, Cherry]

// Practical use: building a SQL IN clause
List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5);
String inClause = ids.stream()
.map(String::valueOf)
.collect(Collectors.joining(", ", "WHERE id IN (", ")"));
System.out.println(inClause); // WHERE id IN (1, 2, 3, 4, 5)
}
}

groupingBy — Group into a Map

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

public class GroupingByExample {
record Student(String name, String dept, int score) {}

public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Alice", "CS", 95),
new Student("Bob", "Math", 78),
new Student("Charlie", "CS", 88),
new Student("David", "Physics", 65),
new Student("Eve", "CS", 92)
);

// Group by department
Map<String, List<Student>> byDept = students.stream()
.collect(Collectors.groupingBy(Student::dept));
System.out.println("CS students: " + byDept.get("CS").size()); // 3

// Group by department and count
Map<String, Long> countByDept = students.stream()
.collect(Collectors.groupingBy(Student::dept, Collectors.counting()));
System.out.println(countByDept); // {CS=3, Math=1, Physics=1}

// Group by department and collect only names
Map<String, List<String>> namesByDept = students.stream()
.collect(Collectors.groupingBy(
Student::dept,
Collectors.mapping(Student::name, Collectors.toList())
));
System.out.println("CS: " + namesByDept.get("CS")); // [Alice, Charlie, Eve]
}
}

3. count — Counting Elements

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

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

long totalCount = numbers.stream().count();
System.out.println("Total: " + totalCount); // 10

long evenCount = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
System.out.println("Even numbers: " + evenCount); // 5

// Count words in a sentence
String text = "the quick brown fox jumps over the lazy dog";
long wordCount = Arrays.stream(text.split("\\s+")).count();
System.out.println("Word count: " + wordCount); // 9
}
}

4. findFirst and findAny — Retrieving a Single Element

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

public class FindExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// findFirst: returns the first element (Optional)
Optional<String> first = names.stream()
.filter(n -> n.length() > 4)
.findFirst();
System.out.println(first); // Optional[Alice]
first.ifPresent(name -> System.out.println("Found: " + name));

// findFirst on empty result
Optional<String> notFound = names.stream()
.filter(n -> n.startsWith("Z"))
.findFirst();
System.out.println(notFound); // Optional.empty
String result = notFound.orElse("Not found");
System.out.println(result); // Not found

// findAny: returns any element (useful in parallel streams)
Optional<String> any = names.parallelStream()
.filter(n -> n.length() > 3)
.findAny();
any.ifPresent(name -> System.out.println("Any: " + name));
}
}

5. anyMatch, allMatch, noneMatch — Matching Conditions

Returns a boolean indicating whether stream elements satisfy a condition.

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

public class MatchExample {
public static void main(String[] args) {
List<Integer> scores = Arrays.asList(85, 90, 78, 92, 65);

// anyMatch: true if at least one element satisfies the condition
boolean hasFail = scores.stream().anyMatch(score -> score < 70);
System.out.println("Has failing score: " + hasFail); // true (65 exists)

// allMatch: true if ALL elements satisfy the condition
boolean allPass = scores.stream().allMatch(score -> score >= 60);
System.out.println("All passing: " + allPass); // true

boolean allHighPass = scores.stream().allMatch(score -> score >= 80);
System.out.println("All high pass: " + allHighPass); // false (78, 65 < 80)

// noneMatch: true if NO element satisfies the condition
boolean noneOver100 = scores.stream().noneMatch(score -> score > 100);
System.out.println("None over 100: " + noneOver100); // true

// Short-circuit evaluation: stops as soon as result is determined
boolean result = scores.stream()
.peek(s -> System.out.println("Checking: " + s))
.anyMatch(s -> s > 88);
// Stops after finding 90 - does not check remaining elements
}
}

6. reduce — Aggregating to a Single Value

Iteratively combines stream elements to produce a single accumulated result.

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

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

// reduce(identity, BinaryOperator): identity is the starting value
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum); // 15

// Using method reference
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Product: " + product); // 120

// reduce without identity: returns Optional (empty stream case)
Optional<Integer> max = numbers.stream()
.reduce((a, b) -> a > b ? a : b);
max.ifPresent(m -> System.out.println("Max: " + m)); // Max: 5

// String concatenation with reduce
List<String> words = Arrays.asList("Hello", " ", "World", "!");
String sentence = words.stream()
.reduce("", String::concat);
System.out.println(sentence); // Hello World!

// Practical: sum of squares
int sumOfSquares = IntStream.rangeClosed(1, 5)
.reduce(0, (acc, n) -> acc + n * n);
System.out.println("Sum of squares 1-5: " + sumOfSquares); // 55
}
}

7. min and max — Finding Extremes

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

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

// max: returns Optional (stream might be empty)
Optional<Integer> max = numbers.stream()
.max(Comparator.naturalOrder());
max.ifPresent(m -> System.out.println("Max: " + m)); // 9

// min
Optional<Integer> min = numbers.stream()
.min(Comparator.naturalOrder());
min.ifPresent(m -> System.out.println("Min: " + m)); // 1

// max on objects using Comparator
record Employee(String name, double salary) {}
List<Employee> employees = Arrays.asList(
new Employee("Alice", 5500),
new Employee("Bob", 4200),
new Employee("Charlie", 6800)
);

Optional<Employee> topEarner = employees.stream()
.max(Comparator.comparingDouble(Employee::salary));
topEarner.ifPresent(e ->
System.out.println("Top earner: " + e.name() + " (" + e.salary() + ")"));
// Top earner: Charlie (6800.0)

// Get just the salary value
double maxSalary = employees.stream()
.mapToDouble(Employee::salary)
.max()
.orElse(0.0);
System.out.println("Max salary: " + maxSalary); // 6800.0
}
}

8. toArray — Converting to Array

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

public class ToArrayExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// toArray() returns Object[]
Object[] objArr = names.stream().toArray();

// toArray(IntFunction<A[]>) returns typed array
String[] strArr = names.stream().toArray(String[]::new);
System.out.println(Arrays.toString(strArr)); // [Alice, Bob, Charlie]

// Primitive stream toArray
int[] ints = IntStream.rangeClosed(1, 5).toArray();
System.out.println(Arrays.toString(ints)); // [1, 2, 3, 4, 5]

// Filter and convert to array
int[] evenArr = IntStream.rangeClosed(1, 10)
.filter(n -> n % 2 == 0)
.toArray();
System.out.println(Arrays.toString(evenArr)); // [2, 4, 6, 8, 10]
}
}

9. Practical Example: Sales Data Analysis

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

public class SalesAnalysis {
record Sale(String product, String region, int amount, int quantity) {}

public static void main(String[] args) {
List<Sale> sales = Arrays.asList(
new Sale("Laptop", "East", 1_200_000, 5),
new Sale("Mouse", "West", 35_000, 20),
new Sale("Keyboard","East", 80_000, 15),
new Sale("Monitor", "West", 520_000, 8),
new Sale("Laptop", "West", 1_200_000, 3),
new Sale("Webcam", "East", 85_000, 10),
new Sale("Monitor", "East", 520_000, 6)
);

// Total revenue
long totalRevenue = sales.stream()
.mapToLong(s -> (long) s.amount() * s.quantity())
.sum();
System.out.printf("Total revenue: %,d%n", totalRevenue);

// Top selling product (by total revenue)
Map<String, Long> revenueByProduct = sales.stream()
.collect(Collectors.groupingBy(
Sale::product,
Collectors.summingLong(s -> (long) s.amount() * s.quantity())
));
Optional<Map.Entry<String, Long>> topProduct = revenueByProduct.entrySet()
.stream()
.max(Map.Entry.comparingByValue());
topProduct.ifPresent(e ->
System.out.printf("Top product: %s (%,d)%n", e.getKey(), e.getValue()));

// Revenue by region
Map<String, Long> revenueByRegion = sales.stream()
.collect(Collectors.groupingBy(
Sale::region,
Collectors.summingLong(s -> (long) s.amount() * s.quantity())
));
revenueByRegion.forEach((region, revenue) ->
System.out.printf(" %s: %,d%n", region, revenue));

// Are all items priced above 10,000?
boolean allOverThreshold = sales.stream()
.allMatch(s -> s.amount() >= 10_000);
System.out.println("All priced >= 10,000: " + allOverThreshold);

// Count of products sold in East region
long eastCount = sales.stream()
.filter(s -> s.region().equals("East"))
.count();
System.out.println("East region sales entries: " + eastCount);
}
}
tip

Pro tip: Choosing the right terminal operation is the key to readable and efficient stream code. Use collect when you need a collection result, reduce for accumulation, anyMatch/allMatch/noneMatch for validation, and findFirst/findAny when you need just one element. Always remember that calling a terminal operation closes the stream permanently.