The Stream API is a tool for declaratively transforming, filtering, and aggregating collections. It changes the for loop + temporary variable code into a one-line pipeline, making the intent clear at a glance.
Learning Objectives
- Create a stream with
stream() - Know the meaning of
map,filter, andreduce - Use
collect(Collectors.toList())andCollectors.groupingBy - Distinguish between intermediate and final operations.
Key concepts
1) Create stream
List<Integer> xs = List.of(1, 2, 3);
Stream<Integer> s = xs.stream();
Stream<Integer> s2 = Stream.of(1, 2, 3);
IntStream s3 = IntStream.rangeClosed(1, 5);
2) Intermediate vs final operation
[ source ] -> map -> filter -> map -> [ terminal ]
\---- middle ----/ (count/collect/forEach/...)
Intermediate operations behave lazily and are not actually executed until the final operation is encountered.
3) map / filter / reduce
int squareSum = Stream.of(1, 2, 3, 4)
.filter(n -> n % 2 == 1)
.map(n -> n * n)
.reduce(0, Integer::sum);
// 1*1 + 3*3 = 10
4) collect and Collectors
import static java.util.stream.Collectors.*;
List<String> upper = list.stream().map(String::toUpperCase).collect(toList());
Map<String, List<Person>> byCity = people.stream().collect(groupingBy(Person::city));
See as example
Example 1 — StreamBasics.java: Creating and outputting a stream
import java.util.List;
public class StreamBasics {
public static void main(String[] args) {
List<String> langs = List.of("Java", "Kotlin", "Scala", "Groovy");
langs.stream()
.forEach(System.out::println);
long count = langs.stream().count();
System.out.println("count=" + count);
}
}
Execution result
Java
Kotlin
Scala
Groovy
count=4
Note: System.out::println is a method reference (details in Part 18).
Example 2 — MapFilter.java: Convert + Filter
import java.util.List;
import java.util.stream.Collectors;
public class MapFilter {
record Product(String name, int price) {}
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Pen", 1000),
new Product("Notebook", 3000),
new Product("Bag", 25000),
new Product("Cup", 5000)
);
List<String> expensiveNames = products.stream()
.filter(p -> p.price() >= 3000)
.map(Product::name)
.collect(Collectors.toList());
System.out.println(expensiveNames);
}
}
Execution result
[Notebook, Bag, Cup]
Note: A call to getter can be expressed as a shorthand method reference, such as Product::name.
Example 3 — Reduce.java: Cumulative sum/maximum value
import java.util.List;
import java.util.Optional;
public class Reduce {
public static void main(String[] args) {
List<Integer> xs = List.of(3, 1, 4, 1, 5, 9, 2, 6);
int sum = xs.stream().reduce(0, Integer::sum);
Optional<Integer> max = xs.stream().reduce(Integer::max);
System.out.println("sum=" + sum);
System.out.println("max=" + max.orElseThrow());
}
}
Execution result
sum=31
max=9
Note: reduce with a starting value always returns a result, but without a starting value it receives Optional compared to an empty stream.
Example 4 — GroupingBy.java : count/average by group
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingBy {
record Student(String name, String dept, int score) {}
public static void main(String[] args) {
List<Student> ss = List.of(
new Student("Alice", "CS", 85),
new Student("Bob", "CS", 70),
new Student("Cathy", "MATH", 92),
new Student("Dan", "MATH", 65),
new Student("Eve", "PHY", 80)
);
Map<String, Double> avgByDept = ss.stream()
.collect(Collectors.groupingBy(
Student::dept,
Collectors.averagingInt(Student::score)));
avgByDept.forEach((d, a) -> System.out.printf("%s -> %.1f%n", d, a));
}
}
Execution results (order may vary)
CS -> 77.5
MATH -> 78.5
PHY -> 80.0
Note: groupingBy is powerful due to its combination of key extraction function + (optional) downstream collector.
Common Mistakes
- Use the stream object twice (consume it once and you're done)
- Unable to terminate after calling
forEachon infinite stream - Using lambda with side effects → dangerous in parallel streams
Collectors.toList()and.toList()(JDK 16+) are slightly different (the latter is unchanged)- Forgetting to convert primitive streams such as
IntStream, resulting in boxing costs
Cleanup
- Streams are declarative data processing pipelines
- Intermediate operations are lazy, final operations are triggered
- Just learn
map/filter/reduceto use 80% Collectorsis powerful when you need Group/Counter
Try it yourself
cd 03_Collection_Generic/14_Stream_API/src
javac MapFilter.java
java MapFilter