PrepZone Logo
PrepZone

Lambdas and Functional Interfaces

Behaviour as a parameter, the built-in functional interfaces, and the four method reference forms.

Why this matters

  • Lambdas are the entry point to streams, CompletableFuture, and essentially every modern Java API. Without them none of those read well.
  • "Behaviour as a parameter" is a design technique, not just shorter syntax, and it replaces a whole category of boilerplate classes.
  • The built-in functional interfaces have specific names that every library expects you to know.
Sourcelist.stream()
filterKeep some
mapTransform each
collectTerminal — runs everything
Nothing runs until the terminal operation. The intermediate steps only describe the work, which is why a pipeline without collect() or forEach() does nothing at all.

From anonymous class to lambda

Java
// Before Java 8
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

// With a lambda
names.sort((a, b) -> Integer.compare(a.length(), b.length()));

// With a method reference and a comparator factory
names.sort(Comparator.comparingInt(String::length));

Each version does the same work. The last one reads as a description of the ordering rather than as code that implements one.

Syntax

Java
// Zero parameters: parentheses required
Runnable task = () -> System.out.println("running");

// One parameter: parentheses optional
Consumer<String> print = value -> System.out.println(value);

// Several parameters
BinaryOperator<Integer> add = (a, b) -> a + b;

// Explicit types, when inference needs help or clarity does
BiFunction<String, Integer, String> repeat = (String text, Integer times) -> text.repeat(times);

// A block body needs an explicit return
Function<String, Integer> parse = text -> {
    String trimmed = text.strip();
    return Integer.parseInt(trimmed);
};

Rules worth stating once

  • An expression body returns its value implicitly; a block body needs return.
  • Parameter types are inferred from the target interface, so they are usually omitted.
  • Captured local variables must be effectively final — assigned once and never reassigned.
  • Instance and static fields may be read and written freely; only locals are restricted.
  • this inside a lambda refers to the enclosing instance, not to the lambda.
Java
int base = 10;
Function<Integer, Integer> addBase = value -> value + base;    // fine: base never changes

int counter = 0;
// Runnable broken = () -> System.out.println(counter);         // error if counter is reassigned later

The restriction exists because the value is copied into the lambda. Allowing reassignment would let the copy and the original disagree, with no way to say which is correct.

The built-in functional interfaces

java.util.function defines the shapes that every modern API uses.

AspectInterfaceShape and typical use
Function<T,R>T in, R out — applymap: transform a value
Predicate<T>T in, boolean out — testfilter: decide inclusion
Consumer<T>T in, nothing out — acceptforEach: a side effect
Supplier<T>Nothing in, T out — getLazy or deferred creation
UnaryOperator<T>T in, T outreplaceAll: transform in place
BinaryOperator<T>Two T in, T outreduce: combine two into one
BiFunction<T,U,R>Two inputs, one outputmerge, compute on a map
  • Function<T,R>

    InterfaceT in, R out — apply
    Shape and typical usemap: transform a value
  • Predicate<T>

    InterfaceT in, boolean out — test
    Shape and typical usefilter: decide inclusion
  • Consumer<T>

    InterfaceT in, nothing out — accept
    Shape and typical useforEach: a side effect
  • Supplier<T>

    InterfaceNothing in, T out — get
    Shape and typical useLazy or deferred creation
  • UnaryOperator<T>

    InterfaceT in, T out
    Shape and typical usereplaceAll: transform in place
  • BinaryOperator<T>

    InterfaceTwo T in, T out
    Shape and typical usereduce: combine two into one
  • BiFunction<T,U,R>

    InterfaceTwo inputs, one output
    Shape and typical usemerge, compute on a map

Each has primitive variants — IntPredicate, ToIntFunction, IntSupplier — that avoid boxing.

Java
Predicate<String> notBlank = text -> !text.isBlank();
Function<String, Integer> length = String::length;
Supplier<List<String>> newList = ArrayList::new;
Consumer<String> log = System.out::println;

They also compose, which is where they stop being verbose and start being useful:

Java
Predicate<String> shortEnough = text -> text.length() <= 20;
Predicate<String> valid = notBlank.and(shortEnough);
Predicate<String> invalid = valid.negate();
Predicate<String> either = notBlank.or(shortEnough);

Function<String, String> strip = String::strip;
Function<String, Integer> stripThenLength = strip.andThen(String::length);
Function<String, Integer> sameThing = length.compose(strip);      // applied right to left

Consumer<String> logThenStore = log.andThen(this::store);

Method references

When a lambda does nothing but call an existing method, a reference says so more clearly.

Java
// 1. A static method
Function<String, Integer> parse = Integer::parseInt;
// equivalent to: text -> Integer.parseInt(text)

// 2. An instance method of a particular object
Consumer<String> print = System.out::println;
// equivalent to: value -> System.out.println(value)

// 3. An instance method of an arbitrary object of a type
Function<String, String> upper = String::toUpperCase;
// equivalent to: text -> text.toUpperCase()     — the receiver becomes the parameter

// 4. A constructor
Supplier<ArrayList<String>> create = ArrayList::new;
Function<Integer, ArrayList<String>> sized = ArrayList::new;      // picks the matching constructor

Form 3 is the one that takes a moment: String::toUpperCase looks like it is missing a receiver, and it is — the first parameter of the functional interface becomes the receiver.

Writing your own functional interface

Any interface with exactly one abstract method qualifies. default and static methods do not count.

Java
@FunctionalInterface
public interface Retry<T> {
    T attempt() throws Exception;             // the single abstract method

    default T withFallback(T fallback) {
        try {
            return attempt();
        } catch (Exception e) {
            return fallback;
        }
    }
}
Java
Retry<String> fetch = () -> httpClient.get(url);
String body = fetch.withFallback("");

The @FunctionalInterface annotation is optional but worth adding: it makes the compiler reject a second abstract method, which protects every lambda already written against the interface.

Behaviour as a parameter

This is the design idea behind the syntax, and the reason it matters beyond brevity.

Java
// Before: one method per variation
public List<Order> findByStatus(Status status) { }
public List<Order> findByCustomer(String id) { }
public List<Order> findByAmountOver(BigDecimal amount) { }

// After: one method, and the caller supplies the rule
public List<Order> find(Predicate<Order> criteria) {
    return orders.stream().filter(criteria).toList();
}

find(order -> order.status() == Status.PENDING);
find(order -> order.customerId().equals(id));
find(order -> order.total().compareTo(threshold) > 0);
find(order -> order.status() == Status.PENDING && order.isOverdue());

The last call is the point: a combination nobody anticipated needs no new method. The library provides the mechanism and the caller provides the policy.

Lambdas versus anonymous classes

AspectLambdaAnonymous class
TargetFunctional interfaces onlyAny interface or abstract class
StateCannot hold fieldsCan hold fields and several methods
this refers toThe enclosing instanceThe anonymous instance
Compiled toAn invokedynamic call siteA separate class file
ShadowingCannot shadow enclosing localsCan
  • Target

    LambdaFunctional interfaces only
    Anonymous classAny interface or abstract class
  • State

    LambdaCannot hold fields
    Anonymous classCan hold fields and several methods
  • this refers to

    LambdaThe enclosing instance
    Anonymous classThe anonymous instance
  • Compiled to

    LambdaAn invokedynamic call site
    Anonymous classA separate class file
  • Shadowing

    LambdaCannot shadow enclosing locals
    Anonymous classCan

Lambdas are not sugar for anonymous classes — the meaning of this differs, and so does the bytecode.

Because lambdas use invokedynamic, the implementation is linked on first use and no extra class file is generated. That keeps the class count down and lets the JIT inline them effectively.

Common misreadings

  • "A lambda is an anonymous class in different clothing." this means something different, and the bytecode is different.
  • "Captured variables must be declared final." They must be effectively final — never reassigned. The keyword is optional.
  • "Lambdas cannot modify anything." They cannot reassign a captured local, but they can mutate the objects those locals point at, and they can write fields.
  • "Any interface works as a lambda target." It needs exactly one abstract method.
  • "@FunctionalInterface is required." It is optional, and useful because it makes the constraint enforced.
  • "Method references are always clearer." String::toUpperCase is. A chain of them with non-obvious receivers is not — use a lambda when it reads better.

Quick recall

Everything you need if you only revisit this box.

  • A lambda implements a functional interface — exactly one abstract method. Java has no function type, which is why the target type decides the meaning.
  • Captured locals must be effectively final, because the value is copied. Fields are unrestricted.
  • this in a lambda is the enclosing instance, unlike in an anonymous class.
  • Know the shapes: Function, Predicate, Consumer, Supplier, UnaryOperator, BinaryOperator, BiFunction — plus primitive variants to avoid boxing.
  • They compose with and, or, negate, andThen, compose. andThen reads left to right.
  • Four method reference forms: static, bound instance, unbound instance, constructor. The unbound form turns the receiver into the first parameter.
  • The real value is behaviour as a parameter: one method plus caller-supplied policy replaces a family of near-identical methods.

Test yourself

Answer these before moving on — recall is what makes it stick.