Skip to content

Lambdas, functional interfaces and streams

A lambda is a short way to write a function without giving it a name or wrapping it in a class.

Builds on Interfaces

Stage 1advanced6 min read

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.

InterfaceMethodMeansExample lambda
Predicate<T>boolean test(T)takes one, returns yes or nos -> s.isEmpty()
Function<T, R>R apply(T)takes one, returns anothers -> s.length()
Consumer<T>void accept(T)takes one, returns nothings -> System.out.println(s)
Supplier<T>T get()takes nothing, returns one() -> new Order()

Two more that show up often:

InterfaceMethodMeans
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-16

Method references

Sometimes a lambda only calls one method that already exists. Then you can name that method with :: and skip the lambda.

LambdaMethod referenceKind
s -> System.out.println(s)System.out::printlninstance method
t -> t.getPrice()Ticket::getPricemethod of the argument
s -> Integer.parseInt(s)Integer::parseIntstatic method
() -> new ArrayList<>()ArrayList::newconstructor

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

OperationWhat 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):

OperationWhat 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 / maxwith 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 levels

Your 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. anyMatch covers 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 final or effectively final (never changed after it is set). So count++ inside a lambda will not compile. Use reduce or collect instead of a counter.
  • Collectors.toMap with duplicate keys throws. Pass a merge function as the third argument if duplicates are possible.
  • peek for real work. It is for debugging. Java may skip it, for example when count() can work out the answer without running the pipeline.

Where you see it in this repo

  • File search: AndPredicate, OrPredicate, SimplePredicate in OOOD/03-unix-file-search-system, the cleanest lambda example here
  • Movie booking: Order.calculateTotalPrice() uses map and reduce, ScreeningManager uses anyMatch
  • Grocery store: CompositeCriteria uses allMatch, Order uses streams for totals
  • Tic tac toe: ScoreTracker sorts entry streams for the leaderboard
  • Blackjack: BlackJackGame uses players.stream()

Related: 08-Generics.md since every functional interface is generic, and 01-Interfaces.md for what a lambda is really implementing.

In this topic

Done with this topic?

How well do you know it?

Previousequals() and hashCode()