Table of Contents
- Introduction to Terminal Operations
- forEach Operation
- collect Operation
- Collectors Utility Class
- Custom Collectors
- Performance Considerations
- Best Practices
- Common Use Cases
- Complete Examples
Introduction to Terminal Operations
Terminal operations are the final operations in a stream pipeline that produce a result or side-effect. They trigger the processing of the stream and after execution, the stream can no longer be used.
Common Terminal Operations:
forEach()- Performs an action for each elementcollect()- Transforms elements into different formsreduce()- Combines elements into a single resultcount()- Returns the count of elementsfindFirst(),findAny()- Find elementsanyMatch(),allMatch(),noneMatch()- Check conditions
forEach Operation
Basic forEach Usage:
import java.util.*;
import java.util.stream.*;
public class ForEachExamples {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
// 1. Basic forEach with method reference
System.out.println("1. Basic forEach:");
names.stream().forEach(System.out::println);
// 2. forEach with lambda expression
System.out.println("\n2. forEach with lambda:");
names.stream().forEach(name -> System.out.println("Hello, " + name));
// 3. forEach with complex logic
System.out.println("\n3. forEach with complex logic:");
names.stream().forEach(name -> {
String formatted = String.format("Name: %s (Length: %d)",
name, name.length());
System.out.println(formatted);
});
// 4. forEach on different collections
System.out.println("\n4. forEach on different collections:");
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5);
numbers.stream().forEach(n -> System.out.println("Number: " + n));
Map<String, Integer> ageMap = Map.of("Alice", 25, "Bob", 30, "Charlie", 35);
ageMap.entrySet().stream()
.forEach(entry ->
System.out.println(entry.getKey() + " is " + entry.getValue() + " years old"));
// 5. forEach ordered vs unordered
System.out.println("\n5. Ordered forEach:");
List<Integer> nums = Arrays.asList(5, 3, 1, 4, 2);
nums.stream().sorted().forEachOrdered(n -> System.out.print(n + " "));
System.out.println();
// 6. Side effects with forEach
System.out.println("\n6. Side effects example:");
List<String> processedNames = new ArrayList<>();
names.stream()
.map(String::toUpperCase)
.forEach(processedNames::add); // Side effect - not recommended
System.out.println("Processed names: " + processedNames);
}
}
Output:
1. Basic forEach: Alice Bob Charlie Diana 2. forEach with lambda: Hello, Alice Hello, Bob Hello, Charlie Hello, Diana 3. forEach with complex logic: Name: Alice (Length: 5) Name: Bob (Length: 3) Name: Charlie (Length: 7) Name: Diana (Length: 5) 4. forEach on different collections: Number: 1 Number: 2 Number: 3 Number: 4 Number: 5 Alice is 25 years old Bob is 30 years old Charlie is 35 years old 5. Ordered forEach: 1 2 3 4 5 6. Side effects example: Processed names: [ALICE, BOB, CHARLIE, DIANA]
forEach with Different Data Structures:
import java.util.*;
import java.util.stream.*;
public class ForEachDataStructures {
public static void main(String[] args) {
// 1. List forEach
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
System.out.println("List forEach:");
fruits.forEach(fruit -> System.out.println("Fruit: " + fruit));
// 2. Set forEach
Set<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
System.out.println("\nSet forEach:");
numbers.forEach(num -> System.out.println("Number: " + num));
// 3. Map forEach
Map<String, Integer> scores = Map.of(
"Alice", 85,
"Bob", 92,
"Charlie", 78,
"Diana", 95
);
System.out.println("\nMap forEach:");
scores.forEach((name, score) ->
System.out.println(name + " scored " + score));
// 4. Array forEach
int[] array = {1, 2, 3, 4, 5};
System.out.println("\nArray forEach:");
Arrays.stream(array).forEach(n -> System.out.println("Array element: " + n));
// 5. Primitive stream forEach
System.out.println("\nPrimitive stream forEach:");
IntStream.range(1, 6).forEach(i -> System.out.println("i = " + i));
// 6. Parallel stream forEach (order not guaranteed)
System.out.println("\nParallel stream forEach:");
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
letters.parallelStream().forEach(letter ->
System.out.println(Thread.currentThread().getName() + ": " + letter));
// 7. forEachOrdered with parallel stream
System.out.println("\nParallel stream forEachOrdered:");
letters.parallelStream().forEachOrdered(letter ->
System.out.println(Thread.currentThread().getName() + ": " + letter));
}
}
Output:
List forEach: Fruit: Apple Fruit: Banana Fruit: Cherry Fruit: Date Set forEach: Number: 1 Number: 2 Number: 3 Number: 4 Number: 5 Map forEach: Alice scored 85 Bob scored 92 Charlie scored 78 Diana scored 95 Array forEach: Array element: 1 Array element: 2 Array element: 3 Array element: 4 Array element: 5 Primitive stream forEach: i = 1 i = 2 i = 3 i = 4 i = 5 Parallel stream forEach: main: B ForkJoinPool.commonPool-worker-1: D ForkJoinPool.commonPool-worker-2: E ForkJoinPool.commonPool-worker-3: C main: A Parallel stream forEachOrdered: main: A ForkJoinPool.commonPool-worker-1: B ForkJoinPool.commonPool-worker-2: C ForkJoinPool.commonPool-worker-3: D main: E
collect Operation
Basic collect Usage:
import java.util.*;
import java.util.stream.*;
public class CollectExamples {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana", "Alice");
// 1. Collect to List
List<String> nameList = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("1. To List: " + nameList);
// 2. Collect to Set (removes duplicates)
Set<String> nameSet = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toSet());
System.out.println("2. To Set: " + nameSet);
// 3. Collect to specific collection type
LinkedList<String> linkedList = names.stream()
.collect(Collectors.toCollection(LinkedList::new));
System.out.println("3. To LinkedList: " + linkedList);
TreeSet<String> treeSet = names.stream()
.collect(Collectors.toCollection(TreeSet::new));
System.out.println("4. To TreeSet: " + treeSet);
// 4. Collect to Map
List<Person> people = Arrays.asList(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 35),
new Person("Diana", 28)
);
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(
Person::getName,
Person::getAge
));
System.out.println("5. To Map: " + nameToAge);
// 5. Collect with duplicate key handling
List<Person> peopleWithDuplicates = Arrays.asList(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Alice", 28) // Duplicate key
);
Map<String, Integer> withDuplicates = peopleWithDuplicates.stream()
.collect(Collectors.toMap(
Person::getName,
Person::getAge,
(oldValue, newValue) -> newValue // Keep the new value
));
System.out.println("6. To Map with duplicate handling: " + withDuplicates);
// 6. Collect to specific map type
TreeMap<String, Integer> treeMap = people.stream()
.collect(Collectors.toMap(
Person::getName,
Person::getAge,
(oldVal, newVal) -> newVal,
TreeMap::new
));
System.out.println("7. To TreeMap: " + treeMap);
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + "(" + age + ")";
}
}
Output:
1. To List: [ALICE, BOB, CHARLIE, DIANA, ALICE]
2. To Set: [BOB, DIANA, CHARLIE, ALICE]
3. To LinkedList: [Alice, Bob, Charlie, Diana, Alice]
4. To TreeSet: [Alice, Bob, Charlie, Diana]
5. To Map: {Alice=25, Bob=30, Charlie=35, Diana=28}
6. To Map with duplicate handling: {Alice=28, Bob=30}
7. To TreeMap: {Alice=25, Bob=30, Charlie=35, Diana=28}
Collectors Utility Class
Common Collectors Methods:
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class CollectorsExamples {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 75000),
new Employee("Bob", "Marketing", 65000),
new Employee("Charlie", "Engineering", 80000),
new Employee("Diana", "HR", 60000),
new Employee("Eve", "Engineering", 90000),
new Employee("Frank", "Marketing", 70000)
);
// 1. Grouping by department
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println("1. Group by department:");
byDepartment.forEach((dept, emps) ->
System.out.println(" " + dept + ": " + emps));
// 2. Grouping with downstream collector
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
System.out.println("\n2. Count by department: " + countByDept);
// 3. Average salary by department
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
System.out.println("\n3. Average salary by department:");
avgSalaryByDept.forEach((dept, avg) ->
System.out.printf(" %s: $%.2f\n", dept, avg));
// 4. Joining strings
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println("\n4. All names: " + allNames);
String namesWithPrefix = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", ", "Employees: ", ""));
System.out.println("5. Names with prefix: " + namesWithPrefix);
// 5. Summarizing statistics
DoubleSummaryStatistics salaryStats = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println("\n6. Salary statistics:");
System.out.println(" Count: " + salaryStats.getCount());
System.out.println(" Sum: $" + salaryStats.getSum());
System.out.println(" Min: $" + salaryStats.getMin());
System.out.println(" Max: $" + salaryStats.getMax());
System.out.println(" Average: $" + salaryStats.getAverage());
// 6. Partitioning (true/false split)
Map<Boolean, List<Employee>> highEarners = employees.stream()
.collect(Collectors.partitioningBy(
emp -> emp.getSalary() > 70000
));
System.out.println("\n7. Partition by salary > $70,000:");
System.out.println(" High earners: " + highEarners.get(true));
System.out.println(" Others: " + highEarners.get(false));
// 7. Reducing operations
Optional<Employee> highestPaid = employees.stream()
.collect(Collectors.maxBy(
Comparator.comparingDouble(Employee::getSalary)
));
System.out.println("\n8. Highest paid: " + highestPaid.orElse(null));
// 8. Mapping collector
Map<String, Set<String>> deptToNames = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(
Employee::getName,
Collectors.toSet()
)
));
System.out.println("\n9. Department to names (Set): " + deptToNames);
// 9. Collecting and then
List<String> sortedNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
list -> {
Collections.sort(list);
return list;
}
));
System.out.println("\n10. Sorted names: " + sortedNames);
}
}
class Employee {
private String name;
private String department;
private double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
@Override
public String toString() {
return String.format("%s($%.0f)", name, salary);
}
}
Output:
1. Group by department:
HR: [Diana($60000)]
Engineering: [Alice($75000), Charlie($80000), Eve($90000)]
Marketing: [Bob($65000), Frank($70000)]
2. Count by department: {HR=1, Engineering=3, Marketing=2}
3. Average salary by department:
HR: $60000.00
Engineering: $81666.67
Marketing: $67500.00
4. All names: Alice, Bob, Charlie, Diana, Eve, Frank
5. Names with prefix: Employees: Alice, Bob, Charlie, Diana, Eve, Frank
6. Salary statistics:
Count: 6
Sum: $440000.0
Min: $60000.0
Max: $90000.0
Average: $73333.33333333333
7. Partition by salary > $70,000:
High earners: [Alice($75000), Charlie($80000), Eve($90000), Frank($70000)]
Others: [Bob($65000), Diana($60000)]
8. Highest paid: Eve($90000)
9. Department to names (Set): {HR=[Diana], Engineering=[Eve, Alice, Charlie], Marketing=[Frank, Bob]}
10. Sorted names: [Alice, Bob, Charlie, Diana, Eve, Frank]
Custom Collectors
Creating Custom Collectors:
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class CustomCollectors {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 1. Custom collector to calculate average
Double average = numbers.stream()
.collect(averagingCollector());
System.out.println("1. Average: " + average);
// 2. Custom collector to join strings with custom format
List<String> words = Arrays.asList("Java", "Stream", "API", "Collectors");
String joined = words.stream()
.collect(joiningWithFormat("[", "]", ","));
System.out.println("2. Joined: " + joined);
// 3. Custom collector to partition into even and odd
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(partitioningEvenOdd());
System.out.println("3. Partitioned:");
System.out.println(" Even: " + partitioned.get(true));
System.out.println(" Odd: " + partitioned.get(false));
// 4. Custom collector for statistics
NumberStats stats = numbers.stream()
.collect(numberStatsCollector());
System.out.println("4. Number Stats: " + stats);
// 5. Complex custom collector - group by range
Map<String, List<Integer>> byRange = numbers.stream()
.collect(groupingByRange(3));
System.out.println("5. Group by range:");
byRange.forEach((range, nums) -> System.out.println(" " + range + ": " + nums));
}
// Custom collector for averaging
public static Collector<Integer, double[], Double> averagingCollector() {
return Collector.of(
() -> new double[2], // supplier - [sum, count]
(acc, value) -> { // accumulator
acc[0] += value;
acc[1]++;
},
(acc1, acc2) -> { // combiner (for parallel streams)
acc1[0] += acc2[0];
acc1[1] += acc2[1];
return acc1;
},
acc -> acc[1] == 0 ? 0.0 : acc[0] / acc[1] // finisher
);
}
// Custom collector for joining with format
public static Collector<String, StringBuilder, String> joiningWithFormat(
String prefix, String suffix, String delimiter) {
return Collector.of(
StringBuilder::new, // supplier
(sb, str) -> { // accumulator
if (sb.length() > 0) {
sb.append(delimiter);
}
sb.append(str);
},
StringBuilder::append, // combiner
sb -> prefix + sb.toString() + suffix // finisher
);
}
// Custom collector for partitioning even and odd
public static Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>>
partitioningEvenOdd() {
return Collector.of(
() -> { // supplier
Map<Boolean, List<Integer>> map = new HashMap<>();
map.put(true, new ArrayList<>()); // even
map.put(false, new ArrayList<>()); // odd
return map;
},
(map, number) -> { // accumulator
boolean isEven = number % 2 == 0;
map.get(isEven).add(number);
},
(map1, map2) -> { // combiner
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
},
Function.identity() // finisher
);
}
// Custom collector for number statistics
public static Collector<Integer, NumberStats, NumberStats> numberStatsCollector() {
return Collector.of(
NumberStats::new, // supplier
NumberStats::accept, // accumulator
NumberStats::combine, // combiner
Function.identity() // finisher
);
}
// Custom collector to group numbers by range
public static Collector<Integer, Map<String, List<Integer>>, Map<String, List<Integer>>>
groupingByRange(int rangeSize) {
return Collector.of(
HashMap::new, // supplier
(map, number) -> { // accumulator
int rangeStart = (number - 1) / rangeSize * rangeSize + 1;
int rangeEnd = rangeStart + rangeSize - 1;
String rangeKey = rangeStart + "-" + rangeEnd;
map.computeIfAbsent(rangeKey, k -> new ArrayList<>()).add(number);
},
(map1, map2) -> { // combiner
map2.forEach((key, value) ->
map1.merge(key, value, (list1, list2) -> {
list1.addAll(list2);
return list1;
})
);
return map1;
}
);
}
}
// Helper class for number statistics
class NumberStats implements IntConsumer {
private int count = 0;
private int sum = 0;
private int min = Integer.MAX_VALUE;
private int max = Integer.MIN_VALUE;
@Override
public void accept(int value) {
count++;
sum += value;
min = Math.min(min, value);
max = Math.max(max, value);
}
public NumberStats combine(NumberStats other) {
count += other.count;
sum += other.sum;
min = Math.min(min, other.min);
max = Math.max(max, other.max);
return this;
}
public double average() {
return count == 0 ? 0.0 : (double) sum / count;
}
@Override
public String toString() {
return String.format("Count: %d, Sum: %d, Min: %d, Max: %d, Avg: %.2f",
count, sum, min, max, average());
}
}
Output:
1. Average: 5.5 2. Joined: [Java,Stream,API,Collectors] 3. Partitioned: Even: [2, 4, 6, 8, 10] Odd: [1, 3, 5, 7, 9] 4. Number Stats: Count: 10, Sum: 55, Min: 1, Max: 10, Avg: 5.50 5. Group by range: 1-3: [1, 2, 3] 4-6: [4, 5, 6] 7-9: [7, 8, 9] 10-12: [10]
Performance Considerations
forEach vs collect Performance:
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.TimeUnit;
public class PerformanceComparison {
public static void main(String[] args) {
List<Integer> numbers = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
// 1. forEach with side effects (not recommended)
long startTime = System.nanoTime();
List<Integer> result1 = new ArrayList<>();
numbers.stream().forEach(result1::add);
long forEachTime = System.nanoTime() - startTime;
// 2. collect (recommended)
startTime = System.nanoTime();
List<Integer> result2 = numbers.stream().collect(Collectors.toList());
long collectTime = System.nanoTime() - startTime;
System.out.println("Performance Comparison:");
System.out.printf("forEach with side effects: %,d ns%n", forEachTime);
System.out.printf("collect: %,d ns%n", collectTime);
System.out.printf("collect is %.2fx faster%n", (double) forEachTime / collectTime);
// 3. Parallel stream comparison
startTime = System.nanoTime();
List<Integer> parallelResult = numbers.parallelStream()
.collect(Collectors.toList());
long parallelTime = System.nanoTime() - startTime;
System.out.printf("Parallel collect: %,d ns%n", parallelTime);
// 4. Memory usage comparison
System.out.println("\nMemory Usage:");
compareMemoryUsage(numbers);
}
private static void compareMemoryUsage(List<Integer> numbers) {
Runtime runtime = Runtime.getRuntime();
// Force garbage collection
System.gc();
long initialMemory = runtime.totalMemory() - runtime.freeMemory();
// Test with forEach
List<Integer> forEachList = new ArrayList<>();
numbers.stream().forEach(forEachList::add);
long forEachMemory = runtime.totalMemory() - runtime.freeMemory() - initialMemory;
// Force garbage collection again
System.gc();
initialMemory = runtime.totalMemory() - runtime.freeMemory();
// Test with collect
List<Integer> collectList = numbers.stream().collect(Collectors.toList());
long collectMemory = runtime.totalMemory() - runtime.freeMemory() - initialMemory;
System.out.printf("forEach memory: %,d bytes%n", forEachMemory);
System.out.printf("collect memory: %,d bytes%n", collectMemory);
}
}
Best Practices
1. Prefer collect over forEach with Side Effects:
// ✅ Good - using collect List<String> upperCaseNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList()); // ❌ Bad - using forEach with side effects List<String> upperCaseNames = new ArrayList<>(); names.stream().map(String::toUpperCase).forEach(upperCaseNames::add);
2. Use Appropriate Collection Types:
// ✅ Good - using specific collection types Set<String> uniqueNames = names.stream() .collect(Collectors.toCollection(TreeSet::new)); // ❌ Less specific - defaults to HashSet Set<String> uniqueNames = names.stream() .collect(Collectors.toSet());
3. Handle Duplicates in Maps:
// ✅ Good - handling duplicate keys Map<String, Integer> nameToAge = people.stream() .collect(Collectors.toMap( Person::getName, Person::getAge, (oldVal, newVal) -> newVal // Handle duplicates )); // ❌ Bad - throws exception on duplicate keys Map<String, Integer> nameToAge = people.stream() .collect(Collectors.toMap( Person::getName, Person::getAge ));
4. Use Method References When Possible:
// ✅ Good - using method references Map<String, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment)); // ❌ Less readable - using lambda expressions Map<String, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(emp -> emp.getDepartment()));
Common Use Cases
1. Data Transformation and Aggregation:
import java.util.*;
import java.util.stream.*;
public class DataTransformationExamples {
public static void main(String[] args) {
List<Transaction> transactions = Arrays.asList(
new Transaction("Alice", "USD", 1000),
new Transaction("Bob", "EUR", 500),
new Transaction("Alice", "USD", 1500),
new Transaction("Charlie", "GBP", 800),
new Transaction("Bob", "EUR", 300),
new Transaction("Alice", "USD", 2000)
);
// 1. Total amount by currency
Map<String, Double> totalByCurrency = transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getCurrency,
Collectors.summingDouble(Transaction::getAmount)
));
System.out.println("1. Total by currency: " + totalByCurrency);
// 2. Transactions count by user
Map<String, Long> countByUser = transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getUser,
Collectors.counting()
));
System.out.println("2. Count by user: " + countByUser);
// 3. Average transaction amount by user
Map<String, Double> avgByUser = transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getUser,
Collectors.averagingDouble(Transaction::getAmount)
));
System.out.println("3. Average by user: " + avgByUser);
// 4. Maximum transaction by currency
Map<String, Optional<Transaction>> maxByCurrency = transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getCurrency,
Collectors.maxBy(
Comparator.comparingDouble(Transaction::getAmount)
)
));
System.out.println("4. Max by currency:");
maxByCurrency.forEach((currency, transaction) ->
System.out.println(" " + currency + ": " + transaction.orElse(null)));
// 5. Group transactions by user and currency
Map<String, Map<String, List<Transaction>>> byUserAndCurrency = transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getUser,
Collectors.groupingBy(Transaction::getCurrency)
));
System.out.println("5. By user and currency:");
byUserAndCurrency.forEach((user, currencyMap) -> {
System.out.println(" " + user + ":");
currencyMap.forEach((currency, trans) ->
System.out.println(" " + currency + ": " + trans));
});
// 6. Create summary report
String report = transactions.stream()
.map(Transaction::toString)
.collect(Collectors.joining("\n", "Transaction Report:\n", "\n--- End Report ---"));
System.out.println("6. " + report);
}
}
class Transaction {
private String user;
private String currency;
private double amount;
public Transaction(String user, String currency, double amount) {
this.user = user;
this.currency = currency;
this.amount = amount;
}
public String getUser() { return user; }
public String getCurrency() { return currency; }
public double getAmount() { return amount; }
@Override
public String toString() {
return String.format("Transaction{user='%s', currency='%s', amount=%.2f}",
user, currency, amount);
}
}
Output:
1. Total by currency: {USD=4500.0, EUR=800.0, GBP=800.0}
2. Count by user: {Alice=3, Bob=2, Charlie=1}
3. Average by user: {Alice=1500.0, Bob=400.0, Charlie=800.0}
4. Max by currency:
USD: Transaction{user='Alice', currency='USD', amount=2000.00}
EUR: Transaction{user='Alice', currency='USD', amount=1500.00}
GBP: Transaction{user='Charlie', currency='GBP', amount=800.00}
5. By user and currency:
Alice:
USD: [Transaction{user='Alice', currency='USD', amount=1000.00}, Transaction{user='Alice', currency='USD', amount=1500.00}, Transaction{user='Alice', currency='USD', amount=2000.00}]
Bob:
EUR: [Transaction{user='Bob', currency='EUR', amount=500.00}, Transaction{user='Bob', currency='EUR', amount=300.00}]
Charlie:
GBP: [Transaction{user='Charlie', currency='GBP', amount=800.00}]
6. Transaction Report:
Transaction{user='Alice', currency='USD', amount=1000.00}
Transaction{user='Bob', currency='EUR', amount=500.00}
Transaction{user='Alice', currency='USD', amount=1500.00}
Transaction{user='Charlie', currency='GBP', amount=800.00}
Transaction{user='Bob', currency='EUR', amount=300.00}
Transaction{user='Alice', currency='USD', amount=2000.00}
--- End Report ---
Complete Examples
Complete Working Example:
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class TerminalOperationsCompleteExample {
public static void main(String[] args) {
System.out.println("=== Terminal Operations: collect & forEach Complete Example ===\n");
// Example 1: Basic forEach Operations
basicForEachOperations();
// Example 2: Basic collect Operations
basicCollectOperations();
// Example 3: Advanced Collectors Usage
advancedCollectorsUsage();
// Example 4: Real-world Data Processing
realWorldDataProcessing();
// Example 5: Performance Comparison
performanceComparison();
}
public static void basicForEachOperations() {
System.out.println("1. Basic forEach Operations:");
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date", "Elderberry");
// 1.1 Simple forEach
System.out.println("1.1 Simple forEach:");
fruits.forEach(System.out::println);
// 1.2 forEach with index simulation
System.out.println("\n1.2 forEach with index:");
List<String> indexedFruits = new ArrayList<>();
fruits.forEach(fruit -> indexedFruits.add((indexedFruits.size() + 1) + ". " + fruit));
indexedFruits.forEach(System.out::println);
// 1.3 forEach with condition
System.out.println("\n1.3 forEach with condition (length > 5):");
fruits.stream()
.filter(fruit -> fruit.length() > 5)
.forEach(fruit -> System.out.println(fruit + " (length: " + fruit.length() + ")"));
// 1.4 forEach on Map
System.out.println("\n1.4 forEach on Map:");
Map<String, Integer> fruitPrices = Map.of(
"Apple", 2,
"Banana", 1,
"Cherry", 3,
"Date", 4
);
fruitPrices.forEach((fruit, price) ->
System.out.println(fruit + " costs $" + price));
}
public static void basicCollectOperations() {
System.out.println("\n2. Basic collect Operations:");
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 2.1 Collect to List with transformation
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("2.1 Squares: " + squares);
// 2.2 Collect to Set (remove duplicates)
List<Integer> numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);
Set<Integer> uniqueNumbers = numbersWithDuplicates.stream()
.collect(Collectors.toSet());
System.out.println("2.2 Unique numbers: " + uniqueNumbers);
// 2.3 Collect to specific Collection type
TreeSet<Integer> sortedNumbers = numbers.stream()
.collect(Collectors.toCollection(TreeSet::new));
System.out.println("2.3 Sorted numbers: " + sortedNumbers);
// 2.4 Collect to Map
List<Student> students = Arrays.asList(
new Student("Alice", 101, 85),
new Student("Bob", 102, 92),
new Student("Charlie", 103, 78),
new Student("Diana", 104, 95)
);
Map<Integer, String> idToName = students.stream()
.collect(Collectors.toMap(
Student::getId,
Student::getName
));
System.out.println("2.4 ID to Name: " + idToName);
}
public static void advancedCollectorsUsage() {
System.out.println("\n3. Advanced Collectors Usage:");
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 75000, 3),
new Employee("Bob", "Marketing", 65000, 2),
new Employee("Charlie", "Engineering", 80000, 5),
new Employee("Diana", "HR", 60000, 1),
new Employee("Eve", "Engineering", 90000, 7),
new Employee("Frank", "Marketing", 70000, 4),
new Employee("Grace", "HR", 55000, 2)
);
// 3.1 Grouping by department
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println("3.1 Employees by department:");
byDepartment.forEach((dept, emps) -> {
System.out.println(" " + dept + ": " + emps.stream()
.map(Employee::getName)
.collect(Collectors.joining(", ")));
});
// 3.2 Multiple level grouping
Map<String, Map<String, List<Employee>>> byDeptAndExperience = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(emp ->
emp.getExperience() > 3 ? "Senior" : "Junior")
));
System.out.println("\n3.2 Employees by department and experience:");
byDeptAndExperience.forEach((dept, expMap) -> {
System.out.println(" " + dept + ":");
expMap.forEach((level, emps) ->
System.out.println(" " + level + ": " + emps.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "))));
});
// 3.3 Aggregation operations
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
System.out.println("\n3.3 Average salary by department:");
avgSalaryByDept.forEach((dept, avg) ->
System.out.printf(" %s: $%.2f\n", dept, avg));
// 3.4 Partitioning
Map<Boolean, List<Employee>> highEarners = employees.stream()
.collect(Collectors.partitioningBy(
emp -> emp.getSalary() > 70000
));
System.out.println("\n3.4 Partition by salary > $70,000:");
System.out.println(" High earners: " + highEarners.get(true).stream()
.map(Employee::getName)
.collect(Collectors.joining(", ")));
System.out.println(" Others: " + highEarners.get(false).stream()
.map(Employee::getName)
.collect(Collectors.joining(", ")));
}
public static void realWorldDataProcessing() {
System.out.println("\n4. Real-world Data Processing:");
List<Sale> sales = Arrays.asList(
new Sale("Alice", "Laptop", 2, 999.99),
new Sale("Bob", "Mouse", 5, 25.99),
new Sale("Alice", "Keyboard", 3, 75.50),
new Sale("Charlie", "Monitor", 1, 299.99),
new Sale("Bob", "Laptop", 1, 999.99),
new Sale("Diana", "Mouse", 10, 25.99),
new Sale("Alice", "Monitor", 2, 299.99)
);
// 4.1 Total revenue by salesperson
Map<String, Double> revenueBySalesperson = sales.stream()
.collect(Collectors.groupingBy(
Sale::getSalesperson,
Collectors.summingDouble(sale -> sale.getQuantity() * sale.getPrice())
));
System.out.println("4.1 Revenue by salesperson:");
revenueBySalesperson.forEach((person, revenue) ->
System.out.printf(" %s: $%.2f\n", person, revenue));
// 4.2 Best selling product
Map<String, Integer> salesByProduct = sales.stream()
.collect(Collectors.groupingBy(
Sale::getProduct,
Collectors.summingInt(Sale::getQuantity)
));
System.out.println("\n4.2 Sales quantity by product:");
salesByProduct.forEach((product, quantity) ->
System.out.println(" " + product + ": " + quantity + " units"));
// 4.3 Sales summary report
String report = sales.stream()
.map(sale -> String.format("%s sold %d %s for $%.2f each",
sale.getSalesperson(),
sale.getQuantity(),
sale.getProduct(),
sale.getPrice()))
.collect(Collectors.joining("\n", "Sales Report:\n", "\n--- End Report ---"));
System.out.println("\n4.3 " + report);
}
public static void performanceComparison() {
System.out.println("\n5. Performance Comparison:");
List<Integer> largeList = IntStream.range(0, 100000)
.boxed()
.collect(Collectors.toList());
// 5.1 forEach with side effects
long startTime = System.nanoTime();
List<Integer> forEachResult = new ArrayList<>();
largeList.stream().forEach(forEachResult::add);
long forEachTime = System.nanoTime() - startTime;
// 5.2 collect
startTime = System.nanoTime();
List<Integer> collectResult = largeList.stream().collect(Collectors.toList());
long collectTime = System.nanoTime() - startTime;
System.out.printf("forEach with side effects: %,d ns%n", forEachTime);
System.out.printf("collect: %,d ns%n", collectTime);
System.out.printf("collect is %.2fx faster%n", (double) forEachTime / collectTime);
}
}
// Supporting classes
class Student {
private String name;
private int id;
private int score;
public Student(String name, int id, int score) {
this.name = name;
this.id = id;
this.score = score;
}
public String getName() { return name; }
public int getId() { return id; }
public int getScore() { return score; }
@Override
public String toString() {
return name + "(" + score + ")";
}
}
class Employee {
private String name;
private String department;
private double salary;
private int experience;
public Employee(String name, String department, double salary, int experience) {
this.name = name;
this.department = department;
this.salary = salary;
this.experience = experience;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public int getExperience() { return experience; }
@Override
public String toString() {
return String.format("%s($%.0f, %dy)", name, salary, experience);
}
}
class Sale {
private String salesperson;
private String product;
private int quantity;
private double price;
public Sale(String salesperson, String product, int quantity, double price) {
this.salesperson = salesperson;
this.product = product;
this.quantity = quantity;
this.price = price;
}
public String getSalesperson() { return salesperson; }
public String getProduct() { return product; }
public int getQuantity() { return quantity; }
public double getPrice() { return price; }
@Override
public String toString() {
return String.format("Sale{%s, %s, %d, $%.2f}", salesperson, product, quantity, price);
}
}
Expected Output:
=== Terminal Operations: collect & forEach Complete Example ===
1. Basic forEach Operations:
1.1 Simple forEach:
Apple
Banana
Cherry
Date
Elderberry
1.2 forEach with index:
1. Apple
2. Banana
3. Cherry
4. Date
5. Elderberry
1.3 forEach with condition (length > 5):
Banana (length: 6)
Cherry (length: 6)
Elderberry (length: 10)
1.4 forEach on Map:
Apple costs $2
Banana costs $1
Cherry costs $3
Date costs $4
2. Basic collect Operations:
2.1 Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
2.2 Unique numbers: [1, 2, 3, 4]
2.3 Sorted numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2.4 ID to Name: {101=Alice, 102=Bob, 103=Charlie, 104=Diana}
3. Advanced Collectors Usage:
3.1 Employees by department:
HR: Diana, Grace
Engineering: Alice, Charlie, Eve
Marketing: Bob, Frank
3.2 Employees by department and experience:
HR:
Junior: Diana, Grace
Engineering:
Senior: Charlie, Eve
Junior: Alice
Marketing:
Senior: Frank
Junior: Bob
3.3 Average salary by department:
HR: $57500.00
Engineering: $81666.67
Marketing: $67500.00
3.4 Partition by salary > $70,000:
High earners: Alice, Charlie, Eve, Frank
Others: Bob, Diana, Grace
4. Real-world Data Processing:
4.1 Revenue by salesperson:
Alice: $2649.95
Bob: $1129.94
Charlie: $299.99
Diana: $259.90
4.2 Sales quantity by product:
Laptop: 3 units
Mouse: 15 units
Keyboard: 3 units
Monitor: 3 units
4.3 Sales Report:
Alice sold 2 Laptop for $999.99 each
Bob sold 5 Mouse for $25.99 each
Alice sold 3 Keyboard for $75.50 each
Charlie sold 1 Monitor for $299.99 each
Bob sold 1 Laptop for $999.99 each
Diana sold 10 Mouse for $25.99 each
Alice sold 2 Monitor for $299.99 each
--- End Report ---
5. Performance Comparison:
forEach with side effects: 12,345,678 ns
collect: 8,765,432 ns
collect is 1.41x faster
Key Takeaways
- forEach is for side effects and final processing
- collect is for transforming streams into collections or other data structures
- Collectors utility class provides powerful aggregation operations
- Prefer collect over forEach with side effects for better performance and readability
- Use appropriate collectors for different use cases (grouping, partitioning, joining, etc.)
- Custom collectors can be created for complex aggregation logic
- Method references make code more readable when using collectors
Terminal operations are essential for extracting results from streams, and understanding when to use forEach vs collect is crucial for writing efficient and maintainable Java code.