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 parenthesesw -> { ... return ...; }: several lines in braces, withreturn
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 withtest(x), returnsbooleanFunction<T, R>: turns aTinto anR, run withapply(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 holdsmap(Book::title)transforms each elementdistinct()removes duplicates,sorted()sorts
At the end comes a step that produces a result:
toList()collects into a new listmapToInt(...)followed bysum()adds upreduce(start, (a, b) -> ...)combines everything into one value: it starts atstartand 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 finalFor sums, use
mapToInt(...).sum()or a normal loop.reversed() in the wrong place
reversed()reverses everything before it. Socomparing(Player::points).thenComparing(Player::name).reversed()also sorts the names backwards. Ifreversed()comes right aftercomparing(...), it only applies to the points.Changing the result of toList()
The list from
toList()and fromList.of(...)can't be changed.addstops withUnsupportedOperationException. 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 writelist.stream()again.