jario_
Back to overview

Chapter 11

Lambdas and streams

So far you've passed values to methods: numbers, strings, 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 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 (chapter 9), 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.