Lambdas and functional interfaces

Java · Practical · 12 min

Modern Java APIs — collections, streams, CompletableFuture, comparators — all take lambdas.

A lambda is a compact way to implement a functional interface: an interface with exactly one abstract method. Instead of an anonymous class, you write just the parameters and the body:

Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
Runnable task = () -> System.out.println("tick");

Parameter types are inferred from the target interface. A single parameter needs no parentheses (s -> s.isBlank()), and a multi-line body uses braces with an explicit return.

java.util.function provides the standard shapes, so you rarely need to declare your own:

  • Function<T, R> — takes T, returns R (apply).
  • Predicate<T> — takes T, returns boolean (test).
  • Consumer<T> — takes T, returns nothing (accept).
  • Supplier<T> — takes nothing, returns T (get).
  • BiFunction<T, U, R>, UnaryOperator<T>, BinaryOperator<T>, plus primitive variants like IntPredicate that avoid boxing.

A method reference is shorthand when a lambda just calls an existing method: String::length, System.out::println, Person::new, this::validate.

Lambdas can read local variables from the enclosing scope only if they are effectively final — never reassigned. This prevents confusing shared mutable state; if you need to accumulate, use a stream reduction or a field instead.

Functional interfaces compose: Predicate.and/or/negate, Function.andThen/compose, Comparator.thenComparing/reversed. Annotate your own with @FunctionalInterface so the compiler enforces the single-abstract-method rule.

Example

import java.util.List;
import java.util.function.*;

public class Lambdas {
    public static void main(String[] args) {
        Predicate<String> isBlank = String::isBlank;
        Predicate<String> hasContent = isBlank.negate();
        Function<String, Integer> length = String::length;
        Function<String, String> shout = s -> s.toUpperCase() + "!";
        Supplier<List<String>> names = () -> List.of("ana", " ", "bo");
        BinaryOperator<Integer> add = Integer::sum;

        int minLength = 2;                 // effectively final: captured below
        Predicate<String> longEnough = s -> s.length() >= minLength;

        for (String n : names.get()) {
            if (hasContent.and(longEnough).test(n)) {
                System.out.println(shout.andThen(length).apply(n) + " " + shout.apply(n));
            }
        }
        // 4 ANA!
        // 3 BO!
        System.out.println(add.apply(2, 3)); // 5
    }
}

Remember

  • A lambda implements a single-abstract-method interface
  • Know Function, Predicate, Consumer, Supplier and their compositions
  • Captured locals must be effectively final

Watch out for

  • Trying to increment a local counter inside a lambda — it won't compile because the variable must be effectively final.

Check yourself

  1. Which functional interface matches `s -> s.isEmpty()` for a String s?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html