Stage 4: Modern Java, lesson 1 of 7

Lambdas and functional interfaces

Intermediate3 min read@since 9Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

A lambda is a short anonymous function: (params) -> expression. It implements a functional interface, meaning an interface with exactly one abstract method.

Built-in functional interfaces in java.util.function:

  • Predicate<T>: takes T, returns boolean (filtering)
  • Function<T, R>: takes T, returns R (transforming)
  • Consumer<T>: takes T, returns nothing (printing, saving)
  • Supplier<T>: takes nothing, returns T (creating)
  • plus BiFunction, UnaryOperator and BinaryOperator

Method references are shorthand when a lambda just calls one method: String::length, System.out::println, User::new.

A lambda can use local variables only if they are effectively final.

Lambda syntax

A lambda is a short anonymous function: parameters, an arrow, then an expression or a block. Parameter types are usually inferred.

Java
Runnable hello = () -> System.out.println("Hi");
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
Function<Integer, Integer> square = x -> x * x;
BinaryOperator<Integer> add = (a, b) -> {
    int sum = a + b;
    return sum;
};

The built-in functional interfaces

java.util.function covers most needs:

  • Function<T, R>: T in, R out. BiFunction takes two inputs.
  • Supplier<T>: nothing in, T out.
  • Consumer<T>: T in, nothing out.
  • Predicate<T>: T in, boolean out.
  • UnaryOperator<T> and BinaryOperator<T>: same type in and out.
  • Primitive versions such as IntPredicate and ToIntFunction avoid boxing.

Method references

When a lambda just calls an existing method, a method reference is shorter. There are four kinds:

  1. Static: Integer::parseInt
  2. On a specific object: System.out::println
  3. On any object of a type: String::toUpperCase
  4. Constructor: ArrayList::new
Java
List<Integer> ids = texts.stream().map(Integer::parseInt).toList();
names.forEach(System.out::println);
List<String> upper = names.stream().map(String::toUpperCase).toList();
Supplier<List<String>> fresh = ArrayList::new;

Capturing variables

A lambda can use local variables from the enclosing method only if they're effectively final (never reassigned). Inside a lambda, this means the enclosing object, unlike in an anonymous class.

Java
int limit = 100;               // effectively final
orders.removeIf(o -> o.amount() > limit);
// limit = 200;                // would make the lambda above a compile error

Composing functions

Functional interfaces have default methods to combine them: andThen and compose for functions, and, or and negate for predicates, and thenComparing and reversed for comparators.

Java
Predicate<Order> paid = Order::isPaid;
Predicate<Order> big = o -> o.amount() > 1000;
orders.stream().filter(paid.and(big.negate())).toList();

Function<String, String> trim = String::strip;
Function<String, Integer> length = trim.andThen(String::length);

Where lambdas show up

Collections (forEach, removeIf, replaceAll, computeIfAbsent, sort), streams, Optional, CompletableFuture, executors and event listeners in frameworks. Once you know lambdas, a lot of modern Java reads naturally.

Example

Java
Predicate<String> isLong = s -> s.length() > 5;
Function<String, Integer> len = String::length;
Consumer<String> print = System.out::println;
Supplier<List<String>> fresh = ArrayList::new;

List<String> words = new ArrayList<>(List.of("stream", "map", "lambda", "var"));
words.removeIf(isLong.negate());                  // keep only long words
words.sort(Comparator.comparing(String::length)
                     .thenComparing(Comparator.reverseOrder()));
words.forEach(print);

@FunctionalInterface
interface Discount { double apply(double price); }
Discount festive = p -> p * 0.8;

Old way vs new way

Java 7 anonymous class
Collections.sort(words, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
});
Java 8+ lambda and method reference
words.sort(Comparator.comparing(String::length));

Common mistake

Changing a local variable inside a lambda (count++) doesn't compile. Use a stream reduction, an AtomicInteger, or restructure the code.

Under the hood

Lambdas are not anonymous classes under the hood. The compiler emits an invokedynamic call that uses LambdaMetafactory to create the implementation at runtime, and lambdas that capture nothing are reused as singletons. Inside a lambda, this refers to the enclosing object, not to the lambda.

Check yourself

Which interface fits x -> x > 10?

How this connects

Part of Java from zero, Job-ready backend developer.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.