Skip to content
Functional Interfaces and Lambdas
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, Streams, and Functional Java

Java 8+ uses functional interfaces — single-abstract-method (SAM) interfaces — as the type for lambdas and method references. Effective Java Items 42–45 cover this in depth.

What's a functional interface?

An interface with EXACTLY ONE abstract method. The @FunctionalInterface annotation enforces it:

@FunctionalInterface
interface Predicate<T> {
    boolean test(T t);
}

Predicate<String> isEmpty = s -> s.isEmpty();
isEmpty.test("hi");          // false

Default methods don't count as "abstract," so Comparator<T> (with compare) is functional even though it has many default methods.

The standard functional interfaces (java.util.function)

Learn these — they're everywhere in stream operations and modern APIs:

Function<T, R>      // T -> R         (apply)
BiFunction<T, U, R> // (T, U) -> R
Predicate<T>        // T -> boolean   (test)
Consumer<T>         // T -> void      (accept)
Supplier<T>         // () -> T        (get)
UnaryOperator<T>    // T -> T         (apply)  - extends Function<T, T>
BinaryOperator<T>   // (T, T) -> T    (apply)

// Primitive specializations to avoid boxing:
IntFunction<R>, ToIntFunction<T>, IntPredicate, IntConsumer, IntSupplier

The primitive variants matter for performance — Stream<Integer> boxes; IntStream does not.

Lambda syntax

x -> x + 1                       // single param, no parens
(x) -> x + 1                     // optional parens
(x, y) -> x + y                  // multi-param
() -> System.out.println("hi")   // no params

(int x, int y) -> x + y          // explicit types (rare)

(x, y) -> {                       // block body
    int sum = x + y;
    return sum * 2;
}

Lambdas can capture effectively final local variables — no var = ... mutation after the lambda is defined.

Method references — the :: operator

For cases where the lambda just calls one method:

// Static method ref
Function<String, Integer> parse = Integer::parseInt;
// equivalent to: s -> Integer.parseInt(s)

// Instance method ref on a specific instance
List<String> list = new ArrayList<>();
Consumer<String> add = list::add;

// Instance method ref on an arbitrary instance
Function<String, Integer> len = String::length;
// equivalent to: s -> s.length()

// Constructor ref
Supplier<ArrayList<String>> factory = ArrayList::new;

Method references are usually clearer than the equivalent lambda. Modern IDEs suggest the conversion.

Composition

Function has helpful default methods:

Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus1 = x -> x + 1;

Function<Integer, Integer> times2ThenPlus1 = times2.andThen(plus1);
Function<Integer, Integer> plus1ThenTimes2 = times2.compose(plus1);

times2ThenPlus1.apply(5);    // 11    (5*2 + 1)
plus1ThenTimes2.apply(5);    // 12    ((5+1) * 2)

Predicate has and, or, negate. Comparator has thenComparing, reversed.

When to use lambdas vs method references vs anonymous classes

Lambdas — for short logic, often single-expression Method references — when the lambda just calls one method (often clearer) Anonymous classes — when you need state, multiple methods, or this to refer to the anonymous instance (lambdas inherit this from the enclosing scope)

Common mistakes

  • Capturing mutable state — won't compile if not effectively final.
  • Anonymous class when lambda would do — verbose, especially for callbacks.
  • Lambda when method reference is clearers -> s.toUpperCase() is uglier than String::toUpperCase.
  • Capturing this — keeps the enclosing object alive longer than needed (memory leaks in long-running tasks).
  • Confusing Function vs UnaryOperator — UnaryOperator is just Function<T, T> with constraints. Use it when input and output types match.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…