Java course menu
Java Streams API: filter, map, reduce, collect and groupingBy
Learn the Java Streams API: creating streams, intermediate operations (filter, map, sorted, distinct, limit), terminal operations (collect, count, reduce, forEach), Collectors toList, joining, groupingBy and counting, primitive streams like IntStream, Optional results, and when to use loops instead.
On this page
The Streams API (Java 8+) lets you process collections by describing what you want, as a pipeline of operations, instead of writing loops. "Take the orders, keep the paid ones, get their amounts, add them up" becomes almost exactly that in code.
A first pipeline#
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(5, 12, 7, 20, 3, 15, 8);
List<Integer> result = nums.stream()
.filter(n -> n > 6) // keep matching elements
.map(n -> n * 10) // transform each element
.sorted() // sort
.toList(); // collect into a list (Java 16+)
System.out.println(result);
System.out.println(nums); // the source is not changed
}
}[70, 80, 120, 150, 200]
[5, 12, 7, 20, 3, 15, 8]A stream pipeline has three parts:
- A source:
collection.stream(),Arrays.stream(array),Stream.of(...),Files.lines(path). - Zero or more intermediate operations, which return a new stream:
filter,map,sorted,distinct,limit,skip. - One terminal operation, which produces a result:
toList,collect,count,sum,forEach,reduce,anyMatch.
Streams are lazy: nothing happens until the terminal operation runs. A stream can be used only once.
Common intermediate operations#
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("banana", "Apple", "cherry", "apple", "fig", "banana");
System.out.println(words.stream().map(String::toLowerCase).distinct().toList());
System.out.println(words.stream().filter(w -> w.length() > 4).count());
System.out.println(words.stream().sorted().limit(3).toList());
System.out.println(words.stream().skip(4).toList());
System.out.println(words.stream().map(String::length).toList());
}
}[banana, apple, cherry, fig]
5
[Apple, apple, banana]
[fig, banana]
[6, 5, 6, 5, 3, 6]Terminal operations: matching, finding, reducing#
import java.util.List;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
List<Integer> marks = List.of(56, 91, 78, 34, 88);
System.out.println(marks.stream().anyMatch(m -> m > 90));
System.out.println(marks.stream().allMatch(m -> m >= 40));
System.out.println(marks.stream().noneMatch(m -> m == 100));
Optional<Integer> firstFail = marks.stream().filter(m -> m < 40).findFirst();
System.out.println(firstFail.orElse(-1));
int total = marks.stream().reduce(0, Integer::sum);
Optional<Integer> best = marks.stream().max(Integer::compare);
System.out.println(total + " " + best.get());
}
}true
false
true
34
347 91Operations that may find nothing, such as findFirst, max and min, return an Optional: a box that either holds a value or is empty. Get the value with orElse(default), ifPresent(...), or get() when you are sure it is present.
Primitive streams: IntStream and friends#
IntStream, LongStream and DoubleStream work directly with primitives and add numeric operations such as sum, average and range:
import java.util.List;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
System.out.println(IntStream.rangeClosed(1, 100).sum());
System.out.println(IntStream.range(0, 5).map(i -> i * i).boxed().toList());
List<Integer> marks = List.of(56, 91, 78, 34, 88);
double avg = marks.stream().mapToInt(Integer::intValue).average().orElse(0);
System.out.println(avg);
var stats = marks.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println(stats.getMin() + " " + stats.getMax() + " " + stats.getSum());
}
}5050
[0, 1, 4, 9, 16]
69.4
34 91 347mapToInt converts a Stream<Integer> to an IntStream; boxed() goes back.
Collectors: grouping, counting and joining#
collect with the Collectors class turns a stream into lists, sets, maps and strings:
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
record Sale(String region, String product, int amount) {}
public class Main {
public static void main(String[] args) {
List<Sale> sales = List.of(
new Sale("North", "Laptop", 110000),
new Sale("South", "Phone", 75000),
new Sale("North", "Phone", 45000),
new Sale("East", "Laptop", 55000),
new Sale("South", "Laptop", 55000));
Map<String, Integer> totalByRegion = sales.stream()
.collect(Collectors.groupingBy(Sale::region, TreeMap::new,
Collectors.summingInt(Sale::amount)));
System.out.println(totalByRegion);
Map<String, Long> countByProduct = sales.stream()
.collect(Collectors.groupingBy(Sale::product, TreeMap::new, Collectors.counting()));
System.out.println(countByProduct);
String regions = sales.stream().map(Sale::region).distinct().sorted()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(regions);
Map<Boolean, List<String>> bigSales = sales.stream()
.collect(Collectors.partitioningBy(s -> s.amount() >= 60000,
Collectors.mapping(Sale::product, Collectors.toList())));
System.out.println(bigSales);
}
}{East=55000, North=155000, South=130000}
{Laptop=3, Phone=2}
[East, North, South]
{false=[Phone, Laptop, Laptop], true=[Laptop, Phone]}Passing TreeMap::new to groupingBy makes the resulting map sorted, which keeps the printed output predictable.
flatMap: flattening nested data#
flatMap turns each element into a stream and merges them all into one:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> sentences = List.of("to be or", "not to be");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.distinct()
.toList();
System.out.println(words);
}
}[to, be, or, not]Streams or loops?#
Streams shine for transform, filter, group and summarize tasks: they are short, readable and hard to get wrong. Plain loops are often clearer when you need early exits with complex logic, several results updated together, or index-based access. Both are good Java; choose whichever reads better.
Common mistakes#
Practice#
Practice exercise
Given List.of("kiwi", "apple", "fig", "banana", "date", "cherry", "apricot"), use streams to: (1) list the words with more than 4 letters in upper case, sorted; (2) group the words by their first letter (sorted map); (3) find the average word length.
Show solution
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class WordStats {
public static void main(String[] args) {
List<String> words = List.of("kiwi", "apple", "fig", "banana", "date", "cherry", "apricot");
List<String> longWords = words.stream()
.filter(w -> w.length() > 4)
.map(String::toUpperCase)
.sorted()
.toList();
System.out.println(longWords);
Map<Character, List<String>> byLetter = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0), TreeMap::new, Collectors.toList()));
System.out.println(byLetter);
double avg = words.stream().mapToInt(String::length).average().orElse(0);
System.out.printf("%.2f%n", avg);
}
}[APPLE, APRICOT, BANANA, CHERRY]
{a=[apple, apricot], b=[banana], c=[cherry], d=[date], f=[fig], k=[kiwi]}
5.00Key takeaway#
Finished this lesson?