Lambdas & Functional Interfaces

A lambda is a short, anonymous function you can pass around like a value: "sort by price", "keep only adults", "when clicked, do this". Lambdas (Java 8+) work wherever a functional interface — an interface with exactly one abstract method — is expected. They're the foundation of streams and modern Java APIs.

From anonymous class to lambda

JAVA
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>(List.of("banana", "fig", "apple"));

        words.sort(new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        });
        System.out.println(words);

        words.sort((a, b) -> b.length() - a.length());
        System.out.println(words);
    }
}
Output
[fig, apple, banana]
[banana, apple, fig]

Both sorts pass "a way to compare two strings". The lambda keeps only what matters: the parameters, an arrow, and the body.

Lambda syntax

FormExample
No parameters() -> "hello"
One parameter (no parentheses needed)x -> x * 2
Several parameters(a, b) -> a + b
Explicit types(String s) -> s.length()
Block bodyx -> { int y = x * 2; return y + 1; }

Parameter types are usually inferred from the context. An expression body returns its value automatically; a block body needs return.

Built-in functional interfaces

JAVA
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> length = s -> s.length();
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Consumer<String> shout = s -> System.out.println(s.toUpperCase() + "!");
        Supplier<String> greeting = () -> "Hello";
        BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
        UnaryOperator<String> trim = String::strip;

        System.out.println(length.apply("lambda"));
        System.out.println(isEven.test(7));
        shout.accept("done");
        System.out.println(greeting.get());
        System.out.println(multiply.apply(6, 7));
        System.out.println("[" + trim.apply("  hi  ") + "]");
    }
}
Output
6
false
DONE!
Hello
42
[hi]
InterfaceTakes → returnsMethod
Function<T, R>T → Rapply
Predicate<T>T → booleantest
Consumer<T>T → nothingaccept
Supplier<T>nothing → Tget
BiFunction<T, U, R>T, U → Rapply
UnaryOperator<T>, BinaryOperator<T>T → T, (T, T) → Tapply

The java.util.function package also has primitive versions (IntPredicate, ToIntFunction…) that avoid boxing.

Method references

JAVA
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("ana", "ben", "mei");
        names.forEach(System.out::println);

        Function<String, String> upper = String::toUpperCase;
        Function<String, Integer> parse = Integer::parseInt;
        Supplier<StringBuilder> maker = StringBuilder::new;

        System.out.println(upper.apply("java") + " " + (parse.apply("40") + 2) + " " + maker.get().append("ok"));
    }
}
Output
ana
ben
mei
JAVA 42 ok

When a lambda just calls an existing method, a method reference is shorter: System.out::println instead of x -> System.out.println(x). Forms: Class::staticMethod, object::method, Class::instanceMethod (called on the argument) and Class::new.

Composing functions

JAVA
import java.util.function.Function;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Function<Double, Double> addVat = p -> p * 1.23;
        Function<Double, Double> discount = p -> p * 0.9;
        System.out.println("%.2f".formatted(discount.andThen(addVat).apply(100.0)));

        Predicate<String> notBlank = s -> !s.isBlank();
        Predicate<String> shortText = s -> s.length() < 10;
        Predicate<String> valid = notBlank.and(shortText);
        System.out.println(valid.test("hello") + " " + valid.test("   ") + " " + valid.negate().test("a very long text"));
    }
}
Output
110.70
true false true

Your own functional interface

JAVA
@FunctionalInterface
interface PriceRule {
    double apply(double price);
}

public class Main {
    static double checkout(double price, PriceRule... rules) {
        for (PriceRule rule : rules) price = rule.apply(price);
        return price;
    }

    public static void main(String[] args) {
        PriceRule tenOff = p -> p - 10;
        PriceRule half = p -> p / 2;
        System.out.println(checkout(80, tenOff, half));
    }
}
Output
35.0

@FunctionalInterface makes the compiler check that the interface has exactly one abstract method. Lambdas can capture local variables only if they're effectively final.

Try it yourself

Write a method that applies a Predicate to filter a list, and call it with different lambdas.

Try it yourself · Main.java
Edit the code, copy it into Main.java and run java Main.java (Java 25+). How to set up Java
Output
[8, 22, 50]
[22, 41, 50]
[Clara]