Skip to content
Laviyx Learn

Try

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.

Advanced12 min readUpdated
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#

Java
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
    }
}
Output
[70, 80, 120, 150, 200]
[5, 12, 7, 20, 3, 15, 8]

A stream pipeline has three parts:

  1. A source: collection.stream(), Arrays.stream(array), Stream.of(...), Files.lines(path).
  2. Zero or more intermediate operations, which return a new stream: filter, map, sorted, distinct, limit, skip.
  3. 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#

Java
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());
    }
}
Output
[banana, apple, cherry, fig]
5
[Apple, apple, banana]
[fig, banana]
[6, 5, 6, 5, 3, 6]

Terminal operations: matching, finding, reducing#

Java
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());
    }
}
Output
true
false
true
34
347 91

Operations 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:

Java
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());
    }
}
Output
5050
[0, 1, 4, 9, 16]
69.4
34 91 347

mapToInt 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:

Java
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);
    }
}
Output
{East=55000, North=155000, South=130000}
{Laptop=3, Phone=2}
[East, North, South]
{false=[Phone, Laptop, Laptop], true=[Laptop, Phone]}
CollectorProduces
Collectors.toList() / toSet()A list / set
Collectors.toMap(keyFn, valueFn)A map
Collectors.joining(sep)One string
Collectors.groupingBy(keyFn)Map<Key, List<T>>
groupingBy(keyFn, counting())Counts per key
groupingBy(keyFn, summingInt(...))Totals per key
Collectors.partitioningBy(predicate)Map<Boolean, List<T>>

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:

Java
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);
    }
}
Output
[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
Java
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);
    }
}
Output
[APPLE, APRICOT, BANANA, CHERRY]
{a=[apple, apricot], b=[banana], c=[cherry], d=[date], f=[fig], k=[kiwi]}
5.00

Key takeaway#

Finished this lesson?