What is a lambda?
A lambda expression is a short piece of code that does one job, and you can pass it around like a value. You write it without making a named class. It fills in the one method of an interface (a functional interface, explained below). That interface tells Java the parameter types and the return type.
Before lambdas, passing a small piece of behaviour to a method meant writing a whole anonymous class (a class with no name, written on the spot):
Comparator<Ticket> byPrice = new Comparator<Ticket>() {
@Override
public int compare(Ticket a, Ticket b) {
return a.getPrice().compareTo(b.getPrice());
}
};With a lambda, the same thing:
Comparator<Ticket> byPrice = (a, b) -> a.getPrice().compareTo(b.getPrice());In simple terms: you delete everything the compiler can work out for itself. That means the class name, the method name, the return keyword and the types.
Lambda syntax
() -> System.out.println("hi") // no arguments
(x) -> x * 2 // one argument
x -> x * 2 // brackets optional for one argument
(x, y) -> x + y // two arguments
(int x, int y) -> x + y // types allowed but rarely needed
x -> { // block body needs braces and return
int doubled = x * 2;
return doubled + 1;
}If the body is one expression, Java returns its value for you. Do not write return or a semicolon inside.
Functional interfaces
A lambda is not magic. It is the code for an interface that has exactly one abstract method (a method with no body). That kind of interface is called a functional interface.
@FunctionalInterface
public interface FilePredicate {
boolean isMatch(File file);
}With only one method, the compiler knows your lambda is the body of that method. The @FunctionalInterface annotation is optional, but add it. If someone adds a second abstract method later, the compiler then reports an error instead of breaking your lambdas.
Your own interfaces work with lambdas too, if they have one abstract method. The file search system in this repo has one, called Predicate (package search, same isMatch method). This note calls it FilePredicate so it is not mixed up with Java's built-in Predicate<T>. The repo implements it with classes such as SimplePredicate, but a lambda could replace them:
// Instead of writing a class for every rule
FilePredicate biggerThan5MB = file -> file.getSize() > 5_000_000;
FilePredicate isXml = file -> file.getFilename().endsWith(".xml");The built-in ones
Java already includes four functional interfaces that you will use all the time. Learn these and you can read most modern Java.
| Interface | Method | Means | Example lambda |
|---|---|---|---|
Predicate<T> | boolean test(T) | takes one, returns yes or no | s -> s.isEmpty() |
Function<T, R> | R apply(T) | takes one, returns another | s -> s.length() |
Consumer<T> | void accept(T) | takes one, returns nothing | s -> System.out.println(s) |
Supplier<T> | T get() | takes nothing, returns one | () -> new Order() |
Two more that show up often:
| Interface | Method | Means |
|---|---|---|
BiFunction<T,U,R> | R apply(T, U) | takes two, returns one |
UnaryOperator<T> | T apply(T) | takes one, returns the same type |
Predicate<String> isEmpty = s -> s.isEmpty();
Function<String, Integer> length = s -> s.length();
Consumer<String> print = s -> System.out.println(s);
Supplier<LocalDate> today = () -> LocalDate.now();
isEmpty.test(""); // true
length.apply("hello"); // 5
print.accept("hi"); // prints hi
today.get(); // 2026-08-16Method references
Sometimes a lambda only calls one method that already exists. Then you can name that method with :: and skip the lambda.
| Lambda | Method reference | Kind |
|---|---|---|
s -> System.out.println(s) | System.out::println | instance method |
t -> t.getPrice() | Ticket::getPrice | method of the argument |
s -> Integer.parseInt(s) | Integer::parseInt | static method |
() -> new ArrayList<>() | ArrayList::new | constructor |
Your Order.java in the movie booking system already uses two of these:
return tickets.stream().map(Ticket::getPrice).reduce(BigDecimal.ZERO, BigDecimal::add);Read it as: take the price of each ticket, then add them all up, starting from zero.
Streams
A stream is a pipeline that data flows through, step by step. Think of a factory line: items go in, each station filters or changes them, and a result comes out. You describe what you want, not how to loop.
// The old way
List<String> names = new ArrayList<>();
for (Player player : players) {
if (player.getScore() > 100) {
names.add(player.getName());
}
}
// The stream way
List<String> names = players.stream()
.filter(player -> player.getScore() > 100)
.map(Player::getName)
.toList();Every stream has three parts:
source intermediate ops terminal op
players.stream() .filter(...).map(...) .toList()
returns a stream returns a result
lazy, does nothing yet this is what runs it
Nothing runs until the terminal operation runs. If you forget it, the whole chain does nothing.
Operations worth knowing
Intermediate (return another stream):
| Operation | What it does |
|---|---|
filter(predicate) | keeps only the elements that match |
map(function) | turns each element into something else |
sorted() | sorts, optionally with a comparator |
distinct() | removes duplicates, uses equals/hashCode |
limit(n) | keeps the first n |
skip(n) | drops the first n |
flatMap(function) | flattens a stream of lists into one stream |
Terminal (produce a result):
| Operation | What it does |
|---|---|
toList() | collects into a list |
collect(Collectors...) | collects into a set, map, joined string |
forEach(consumer) | runs something for each element |
count() | how many |
anyMatch(predicate) | is at least one a match |
allMatch(predicate) | are they all a match |
noneMatch(predicate) | is none a match |
findFirst() | returns an Optional with the first element |
reduce(start, op) | folds everything into one value |
min / max | with a comparator, returns an Optional |
Examples from this repo, explained
// ScreeningManager: is this seat already taken?
return tickets.stream().anyMatch(ticket -> ticket.getSeat().equals(seat));Stops as soon as it finds one match. Cleaner than a loop with a break.
// AndPredicate: do all sub rules pass?
return operands.stream().allMatch(predicate -> predicate.isMatch(inputFile));
// OrPredicate: does any sub rule pass?
return operands.stream().anyMatch(predicate -> predicate.isMatch(inputFile));This is the Composite pattern with two one line methods.
// Order: total price of all tickets
return tickets.stream().map(Ticket::getPrice).reduce(BigDecimal.ZERO, BigDecimal::add);reduce needs a starting value (BigDecimal.ZERO) and a way to combine two values (BigDecimal::add).
Collectors
collect is the terminal operation that can build many kinds of results.
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
// join into one string
String csv = names.stream().collect(Collectors.joining(", "));
// group into a map
Map<VehicleSize, List<Vehicle>> bySize =
vehicles.stream().collect(Collectors.groupingBy(Vehicle::getSize));
// count per group
Map<VehicleSize, Long> countBySize =
vehicles.stream().collect(Collectors.groupingBy(Vehicle::getSize, Collectors.counting()));
// build a lookup map
Map<String, Vehicle> byPlate =
vehicles.stream().collect(Collectors.toMap(Vehicle::getLicensePlate, v -> v));groupingBy replaces the most code. In one line it does what a loop would do with a map, a null check and a list for each key.
Sorting with comparators
tickets.sort(Comparator.comparing(Ticket::getPrice)); // by price
tickets.sort(Comparator.comparing(Ticket::getPrice).reversed()); // high to low
tickets.sort(Comparator.comparing(Ticket::getShowTime)
.thenComparing(Ticket::getPrice)); // two levelsYour ScoreTracker in tic tac toe sorts entries this way to build a leaderboard.
Lambdas replace small strategy classes
This links to the Strategy design pattern. If a Strategy has only one method, you do not need a new class for each rule.
// Classic Strategy: one class per rule
class PriceAbove100 implements Criteria {
public boolean isApplicable(Item item) { return item.getPrice() > 100; }
}
// Same thing as a lambda
Criteria priceAbove100 = item -> item.getPrice() > 100;Use a real class when the strategy keeps data, deserves a name in your domain, or takes more than a couple of lines. Use a lambda for a one line rule.
The same applies to Command (Runnable is a functional interface) and to simple Observers.
When not to use streams
- Simple loops.
for (String s : list) System.out.println(s);is clearer than a stream. - When you need to modify the source collection while going through it.
- When you need the index. Streams do not give you one easily.
- When you need to break out early with complex conditions.
anyMatchcovers the simple case, beyond that a loop is clearer. - In very hot loops. Streams have a small overhead. It rarely matters, but it is not zero.
Readability is the point. If the stream version is harder to read, write the loop.
Common mistakes
- Forgetting the terminal operation.
list.stream().filter(...)on its own does nothing at all. - Reusing a stream. A stream can only be consumed once. Call
.stream()again for a second pass. - Changing an outside variable inside a lambda. A lambda can only read a local variable that is
finalor effectively final (never changed after it is set). Socount++inside a lambda will not compile. Usereduceorcollectinstead of a counter. Collectors.toMapwith duplicate keys throws. Pass a merge function as the third argument if duplicates are possible.peekfor real work. It is for debugging. Java may skip it, for example whencount()can work out the answer without running the pipeline.
Where you see it in this repo
- File search:
AndPredicate,OrPredicate,SimplePredicateinOOOD/03-unix-file-search-system, the cleanest lambda example here - Movie booking:
Order.calculateTotalPrice()usesmapandreduce,ScreeningManagerusesanyMatch - Grocery store:
CompositeCriteriausesallMatch,Orderuses streams for totals - Tic tac toe:
ScoreTrackersorts entry streams for the leaderboard - Blackjack:
BlackJackGameusesplayers.stream()
Related: 08-Generics.md since every functional interface is generic, and 01-Interfaces.md for what a lambda is really implementing.