Generics, Lambdas and Streams · Lesson 36 of 42
The Stream API
Streams let you describe what you want from a collection – "the names of IT employees earning over ₹80,000, sorted" – instead of writing the loops that do it. The code reads like a sentence, and stream questions are now a standard part of Java interviews.
In this lesson
- How a stream pipeline works
- Intermediate operations: filter, map, sorted, distinct, limit
- Terminal operations: collect, reduce, count, anyMatch
- Collectors: toList, groupingBy, counting, joining – and flatMap
A pipeline
import java.util.List;
public class Main {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Asha", "IT", 85000), new Employee("Ben", "HR", 60000),
new Employee("Chen", "IT", 92000), new Employee("Dia", "Sales", 70000),
new Employee("Eli", "IT", 78000));
List<String> result = staff.stream() // 1. source
.filter(e -> e.dept().equals("IT")) // 2. keep IT
.filter(e -> e.salary() > 80000) // earning over 80k
.map(Employee::name) // 3. transform to names
.sorted() // sort
.toList(); // 4. terminal: collect
System.out.println(result);
}
}
[Asha, Chen]
source → intermediate ops → terminal op
- Sourcelist.stream(), Arrays.stream(arr), Stream.of(...)
- Intermediate (lazy)filter, map, sorted, distinct, limit – return a new stream
- TerminaltoList, collect, reduce, count, forEach – runs the pipeline
- Streams don't change the source collection.
- Intermediate operations are lazy: nothing runs until a terminal operation is called.
- A stream can be consumed only once.
Common operations
import java.util.List;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(5, 3, 8, 3, 9, 1, 8, 6);
System.out.println(nums.stream().distinct().sorted().toList());
System.out.println(nums.stream().map(n -> n * n).limit(3).toList());
System.out.println(nums.stream().filter(n -> n % 2 == 0).count());
System.out.println(nums.stream().mapToInt(Integer::intValue).sum());
System.out.println(nums.stream().mapToInt(n -> n).average().getAsDouble());
System.out.println(nums.stream().max(Integer::compare).get());
System.out.println(nums.stream().anyMatch(n -> n > 8) + " " + nums.stream().allMatch(n -> n > 0));
System.out.println(nums.stream().reduce(0, Integer::sum));
System.out.println(IntStream.rangeClosed(1, 5).boxed().toList());
}
}
[1, 3, 5, 6, 8, 9] [25, 9, 64] 3 43 5.375 9 true true 43 [1, 2, 3, 4, 5]
Collectors: grouping and summarising
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Main {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Asha", "IT", 85000), new Employee("Ben", "HR", 60000),
new Employee("Chen", "IT", 92000), new Employee("Dia", "Sales", 70000),
new Employee("Eli", "IT", 78000), new Employee("Fay", "HR", 64000));
Map<String, Long> headcount = staff.stream()
.collect(Collectors.groupingBy(Employee::dept, TreeMap::new, Collectors.counting()));
System.out.println(headcount);
Map<String, Double> avgPay = staff.stream()
.collect(Collectors.groupingBy(Employee::dept, TreeMap::new, Collectors.averagingDouble(Employee::salary)));
System.out.println(avgPay);
Map<String, List<String>> names = staff.stream()
.collect(Collectors.groupingBy(Employee::dept, TreeMap::new,
Collectors.mapping(Employee::name, Collectors.toList())));
System.out.println(names);
Map<Boolean, Long> above75k = staff.stream()
.collect(Collectors.partitioningBy(e -> e.salary() > 75000, Collectors.counting()));
System.out.println(above75k);
System.out.println(staff.stream().map(Employee::name).collect(Collectors.joining(", ", "[", "]")));
}
}
{HR=2, IT=3, Sales=1}
{HR=62000.0, IT=85000.0, Sales=70000.0}
{HR=[Ben, Fay], IT=[Asha, Chen, Eli], Sales=[Dia]}
{false=3, true=3}
[Asha, Ben, Chen, Dia, Eli, Fay]flatMap: flattening nested data
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<List<String>> skills = List.of(List.of("Java", "SQL"), List.of("Java", "Spring"), List.of("Git"));
System.out.println(skills.stream().flatMap(List::stream).distinct().sorted().toList());
List<String> lines = List.of("to be or", "not to be");
long words = lines.stream().flatMap(l -> Arrays.stream(l.split(" "))).count();
System.out.println(words + " words");
}
}
[Git, Java, SQL, Spring] 6 words
Interview-style stream questions
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
String s = "programming";
// 1. Character frequency (in order of first appearance)
Map<Character, Long> freq = s.chars().mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()));
System.out.println(freq);
// 2. First non-repeated character
System.out.println(freq.entrySet().stream().filter(e -> e.getValue() == 1).map(Map.Entry::getKey).findFirst().orElse('-'));
// 3. Second highest distinct number
List<Integer> nums = List.of(12, 45, 7, 45, 30);
System.out.println(nums.stream().distinct().sorted((a, b) -> b - a).skip(1).findFirst().orElseThrow());
// 4. Duplicates
System.out.println(nums.stream().filter(n -> nums.indexOf(n) != nums.lastIndexOf(n)).distinct().toList());
}
}
{p=1, r=2, o=1, g=2, a=1, m=2, i=1, n=1}
p
30
[45]Common mistakes with streams
- Reusing a stream: a second terminal operation throws
IllegalStateException. - Changing external variables inside
forEachormap– keep lambdas free of side effects and use collectors instead. - Using streams for everything: a simple loop is sometimes clearer, especially with
breakor complex state. parallelStream()is not automatically faster – it helps only for large, CPU-heavy work.
Practice
Try it yourself
From ["apple", "banana", "kiwi", "cherry", "fig"], list words longer than 4 letters in upper case.
Show solution
import java.util.List; public class Main { public static void main(String[] args) { List<String> fruits = List.of("apple", "banana", "kiwi", "cherry", "fig"); System.out.println(fruits.stream().filter(f -> f.length() > 4).map(String::toUpperCase).toList()); } }[APPLE, BANANA, CHERRY]
Sum the squares of the odd numbers from 1 to 9.
Show solution
import java.util.stream.IntStream; public class Main { public static void main(String[] args) { System.out.println(IntStream.rangeClosed(1, 9).filter(n -> n % 2 == 1).map(n -> n * n).sum()); } }165
Quick check
When does a stream pipeline actually run?
Only when a terminal operation (toList, collect, count, forEach…) is called.
What is the difference between map and flatMap?
map transforms each element into one value; flatMap transforms each into a stream and flattens the results into one stream.
Which collector groups elements by a key?
Collectors.groupingBy
Summary
- A stream is source → lazy intermediate operations → one terminal operation.
- filter, map, sorted, distinct, limit shape data; collect, reduce, count, anyMatch finish it.
- groupingBy, partitioningBy, counting, joining and flatMap solve most real reporting tasks.
Interview practice: Java Stream API coding questions.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.