Java Lambdas and Streams: filter, map, Comparator Explained

Updated on October 22, 2026

Lambdas and streams in Java extend what you can pass to methods: usually these are values like numbers, strings and lists. With a lambda you pass a piece of behaviour, such as "add 50" or "is longer than 4 characters". Streams build on that: chains of steps like filtering, transforming and adding up, without writing your own loop.

Lambdas and method references

A lambda is a function without a name. The parameters go left of the arrow, the result on the right:

  • p -> p + 50: one parameter
  • (a, b) -> a + b: two parameters, in parentheses
  • w -> { ... return ...; }: several lines in braces, with return

A lambda fits wherever an interface with exactly one method is expected. The most common ones from java.util.function:

  • Predicate<T>: a condition, run with test(x), returns boolean
  • Function<T, R>: turns a T into an R, run with apply(x)

Lists (see lists and maps in Java) have methods that take a lambda: replaceAll replaces every element, removeIf removes all for which the condition holds. If a lambda only calls an existing method, a method reference is shorter: String::length stands for s -> s.length(), Math::max for (a, b) -> Math.max(a, b).

import java.util.ArrayList;import java.util.List;import java.util.function.Function;import java.util.function.Predicate;
public class Main {    public static void main(String[] args) {        List<Integer> prices = new ArrayList<>(List.of(250, 399, 120));        prices.replaceAll(p -> p + 50);        System.out.println(prices);
        Predicate<String> isLong = w -> w.length() > 4;        System.out.println(isLong.test("Java"));        System.out.println(isLong.test("Lambda"));
        Function<String, String> shout = s -> s.toUpperCase() + "!";        System.out.println(shout.apply("hello"));
        Function<String, Integer> length = String::length;        System.out.println(length.apply("stream"));
        List<String> drinks = new ArrayList<>(List.of("tea", "coffee", "juice", "milk"));        drinks.removeIf(d -> d.length() < 4);        System.out.println(drinks);    }}

Streams

list.stream() turns a list into a stream of elements that flow through one after another. You chain the steps with dots:

  • filter(b -> ...) keeps only elements for which the condition holds
  • map(Book::title) transforms each element
  • distinct() removes duplicates, sorted() sorts

At the end comes a step that produces a result:

  • toList() collects into a new list
  • mapToInt(...) followed by sum() adds up
  • reduce(start, (a, b) -> ...) combines everything into one value: it starts at start and combines the result so far, a, with the next element, b

The original list stays unchanged.

import java.util.List;
record Book(String title, String genre, int pages) {}
public class Main {    public static void main(String[] args) {        List<Book> books = List.of(                new Book("Dune", "sci-fi", 600),                new Book("Emma", "classic", 470),                new Book("Neuromancer", "sci-fi", 270),                new Book("Persuasion", "classic", 250),                new Book("Hyperion", "sci-fi", 480));
        List<String> longBooks = books.stream()                .filter(b -> b.pages() > 400)                .map(Book::title)                .toList();        System.out.println(longBooks);
        int totalPages = books.stream().mapToInt(Book::pages).sum();        System.out.println("Pages: " + totalPages);
        int maxPages = books.stream().map(Book::pages).reduce(0, Math::max);        System.out.println("Longest: " + maxPages);
        List<String> genres = books.stream()                .map(Book::genre)                .distinct()                .sorted()                .toList();        System.out.println(genres);    }}

Comparator and grouping

With Comparable (see inheritance and interfaces in Java), a class has one fixed order. A comparator is a sorting rule you pass in from outside:

  • Comparator.comparing(Player::points) sorts ascending by points
  • .reversed() reverses the order
  • .thenComparing(Player::name) decides ties

You pass it to stream().sorted(...) or to list.sort(...).

collect(Collectors.groupingBy(Player::team, ...)) splits the elements into groups and returns a map: team → result. The second collector says what comes out per group, such as Collectors.summingInt(...) for a sum or Collectors.counting() for the count (as a Long).

import java.util.Comparator;import java.util.List;import java.util.Map;import java.util.stream.Collectors;
record Player(String name, String team, int points) {}
public class Main {    public static void main(String[] args) {        List<Player> players = List.of(                new Player("Mia", "red", 30),                new Player("Leo", "blue", 45),                new Player("Ana", "red", 45),                new Player("Ben", "blue", 20));
        List<Player> ranking = players.stream()                .sorted(Comparator.comparing(Player::points).reversed()                        .thenComparing(Player::name))                .toList();        for (Player p : ranking) {            System.out.println(p.name() + " " + p.points());        }
        Map<String, Integer> perTeam = players.stream()                .collect(Collectors.groupingBy(Player::team,                        Collectors.summingInt(Player::points)));        System.out.println("red: " + perTeam.get("red"));        System.out.println("blue: " + perTeam.get("blue"));
        Map<String, Long> sizes = players.stream()                .collect(Collectors.groupingBy(Player::team, Collectors.counting()));        System.out.println("players in red: " + sizes.get("red"));    }}

Common mistakes

  • Changing a local variable in a lambda

    A lambda may read local variables from outside, but not change them:

    int sum = 0;prices.forEach(p -> sum += p);// local variables referenced from a lambda expression// must be final or effectively final

    For sums, use mapToInt(...).sum() or a normal loop.

  • reversed() in the wrong place

    reversed() reverses everything before it. So comparing(Player::points).thenComparing(Player::name).reversed() also sorts the names backwards. If reversed() comes right after comparing(...), it only applies to the points.

  • Changing the result of toList()

    The list from toList() and from List.of(...) can't be changed. add stops with UnsupportedOperationException. If you need a list you can change, copy it: new ArrayList<>(list).

  • A stream is used up after one pass

    You can only use a stream once. If you ask the same stream for a second result, you get an IllegalStateException. For a second evaluation, just write list.stream() again.

Tasks on this topic

Practise what you learned right in your browser. Tests check your solution instantly.