Skip to content
Lesson 7 of 7

Step 1 of 7 · Reading · ~3 min

Learn

Generics, Streams, and Functional Java

Functional Interfaces and Lambdas

In Java Intermediate you used the standard Function, Predicate, Supplier and friends and wrote method references. This lesson is about designing your own functional interfaces with default and static methods, what a lambda actually captures, laziness through Supplier, and the checked-exception problem. You will be able to build a small combinator library where several lambdas compose into one.

Your own functional interface

Any interface with exactly one abstract method can be the target of a lambda. Add default methods for combinators and static methods for factories:

@FunctionalInterface
interface Transform<A, B> {
    B apply(A in);                                            // the one abstract method

    default <C> Transform<A, C> then(Transform<B, C> next) {  // combinator: returns a new lambda
        return in -> next.apply(apply(in));
    }

    static <A> Transform<A, A> identity() { return a -> a; }  // factory
}

Transform<String, Integer> len = String::length;
Transform<String, String> label = len.then(n -> n + " chars");
label.apply("lambda");                                        // "6 chars"

@FunctionalInterface makes the compiler reject a second abstract method. Inside then, the lambda calls apply(in) on the enclosing object: a lambda does not have its own this; this and unqualified method names mean the enclosing instance. An anonymous class has its own this. That is the one real semantic difference between them.

Return a lambda when behaviour depends on a value you have in hand. A function that returns a function is currying:

Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;
Function<Integer, Integer> plusTen = add.apply(10);     // partially applied
plusTen.apply(5);                                       // 15

What a lambda captures

A lambda can read local variables only if they are effectively final, because it keeps a copy of the value. A counter that must change from inside a lambda has to live in an object:

int hits = 0;
list.forEach(x -> hits++);                    // error: hits is not effectively final
AtomicInteger hits2 = new AtomicInteger();
list.forEach(x -> hits2.incrementAndGet());   // fine: the reference is final, the object mutates

Fields are different: a lambda reads this.field live, not a copy.

Laziness with Supplier

Arguments are evaluated before the call. opt.orElse(load()) calls load() even when the Optional has a value; opt.orElseGet(() -> load()) calls it only when needed. Passing a Supplier<String> to a logging method defers building an expensive message until the level check passes. Short-circuit operators rely on the same idea: Predicate.and(b) never calls b if the first test is false.

Checked exceptions

The standard interfaces declare no throws, so map(Class::forName) does not compile. Define a throwing variant and adapt it once:

@FunctionalInterface
interface ThrowingFunction<T, R> { R apply(T t) throws Exception; }

static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> fn) {
    return t -> {
        try { return fn.apply(t); }
        catch (Exception e) { throw new RuntimeException(e); }
    };
}
// names.stream().map(unchecked(Class::forName)) now compiles

Identity elements

When you fold many lambdas into one, the empty case needs a neutral element: x -> true for combining predicates with and, x -> x for composing functions. Without one, an empty list has no meaningful result.

Common mistakes

  • Evaluating all conditions when the first failure should end the check. Extra work and extra side effects.
  • Using orElse(expensiveCall()). The call always runs.
  • Assuming a lambda captures a variable, not its value. The compiler refuses reassigned locals.
  • Expecting this to be the lambda. It is the enclosing object.

Your exercise

Finish a validation Rule<T> interface: build a rule from a predicate and a message, chain two rules so the first failure wins, and fold a list of rules into one. The starter counts how many predicate calls are made, so a rule that checks more than it must is visible in the output.

Discussion

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

Sign in to post a comment or reply.

Loading…