Module 9 · Modern Java: Functional Style & Concurrency ⏱ 19 min

Functional Interfaces and Lambdas

By the end of this lesson you will be able to:
  • Recognise the core functional interfaces from java.util.function and what each one's single method does
  • Use target typing to explain why the same lambda means different things under different target types
  • Predict the output of composed functions built with andThen and compose

You already met lambdas as compact stand-ins for anonymous inner classes, together with the rule that each one targets an interface with a single abstract method. That rule is small, but it is the hinge on which Java's modern style turns — because once a language commits to single-method interfaces, a whole vocabulary of tiny, composable types becomes possible.

Java ships that vocabulary ready-made, in the java.util.function package. Four interfaces do the lion's share of the work across the standard library: Function, Predicate, Consumer, and Supplier. Streams speak exclusively in these types, so reading any pipeline fluently means recognising each one on sight. This lesson makes that vocabulary concrete, then tackles the single behaviour that catches nearly everyone — the order in which composed functions run.

flowchart TD
  SRC["A functional interface has exactly one abstract method"]
  F["Function<br/>apply returns a transformed value"] --> FT["turns one input into one output"]
  P["Predicate<br/>test returns a boolean"] --> PT["answers a yes or no question"]
  C["Consumer<br/>accept returns nothing"] --> CT["uses a value, gives back void"]
  SUP["Supplier<br/>get takes nothing"] --> ST["hands back a value with no input"]
  style SRC fill:#1e293b,color:#fff
The four workhorse functional interfaces, each defined by one abstract method whose name signals what it returns.

Each of those four is a functional interface defined by one abstract method. Function transforms a value through apply; Predicate tests one through test, which returns a boolean; Consumer acts on one through accept, returning void; Supplier produces one through get, taking no arguments. The method name is the contract — when a signature reads test, you already know a boolean is coming back.

A functional interface may still carry default and static methods freely; only the abstract method count matters. The @FunctionalInterface annotation is optional but worth adding to your own: it tells the compiler to reject any second abstract method, turning a silent slip into a build error. Nearly every interface in java.util.function carries it for exactly that reason.

Main.java — a Function and a Predicate used as the targets of two lambdas. Real, compiling Java.
import java.util.function.Function;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> length = s -> s.length();
        Predicate<String> isEmpty = s -> s.isEmpty();
        System.out.println(length.apply("stream"));
        System.out.println(isEmpty.test(""));
    }
}

Consumer and Supplier sit at the edges where data enters or leaves your code. A Supplier takes no arguments and hands back a value — think of a factory method, or a lazy source that produces items on demand. A Consumer takes a value and returns nothing, so it is the natural shape for a side effect such as printing a line. When you meet forEach later in the Stream API, the action it runs on each element is a Consumer, and the thing that yields the next element can be a Supplier.

Knowing the shapes pays off the moment you read a method signature. See a parameter typed Consumer<String> and you know the method will eventually do something with each string and discard the result — no return value to track, no transformation to follow.

Target typing: a lambda takes its type from its context

A lambda has no standalone type. Its type is whatever functional interface the surrounding context expects — a mechanism called target typing — and that is why the identical expression x -> x + 1 can mean two genuinely different things.

Hand it to a Function<Integer, Integer> and the literal 1 is a number, so the lambda adds. Hand the same body to a Function<String, String> and Java now treats the incoming value as text, so the plus sign concatenates instead of adding. The lambda's text never changed; the target did, and the target chose the meaning.

That is also why Object o = x -> x + 1; will not compile. With no functional interface within reach, the compiler has nothing to measure the lambda against, and it refuses to guess. A lambda always needs a target.

flowchart LR
  L["the same body<br/>x becomes x plus 1"] --> FA["target is a numeric Function<br/>so the 1 is added"]
  L --> FB["target is a text Function<br/>so the 1 is appended"]
  style L fill:#3776ab,color:#fff
Target typing: the same lambda body resolves differently depending on which functional interface it is assigned to.
Main.java — the same lambda body, two target types, two meanings. Real, compiling Java.
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> addOne = x -> x + 1;
        Function<String, String> tagOne = x -> x + 1;
        System.out.println(addOne.apply(40));
        System.out.println(tagOne.apply("score"));
    }
}

Composition: wiring small functions into one

Functional interfaces are not just targets for single lambdas — several come with default methods that build new interfaces out of existing ones, with no boilerplate. Function offers andThen and compose, each returning a fresh Function that represents a two-step pipeline. Predicate offers and, or, and negate, which combine tests the way && and ! combine booleans.

Composition is what makes the functional style scale past toy examples. Instead of one large lambda that does three things, you wire three small ones together and hand the combined result to a stream. Each piece stays readable and testable on its own. The catch — and it is a real one — is that andThen and compose run their steps in opposite orders, and mixing them up changes the answer.

flowchart LR
  AT["f.andThen(g).apply(x)"] --> S1["runs f first<br/>then g"]
  CO["f.compose(g).apply(x)"] --> S2["runs g first<br/>then f"]
  style AT fill:#3776ab,color:#fff
  style CO fill:#b45309,color:#fff
andThen and compose feed the same input through f and g in opposite orders, so they produce different results.
Exercise

What does this program print? Read it carefully and type the exact output (two lines).

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> doubleIt = n -> n * 2;
        System.out.println(doubleIt.apply(3));
        System.out.println(doubleIt.apply(10));
    }
}
Exercise

A functional interface already has one abstract method. Which of these changes stops it being usable as a lambda target?

Exercise

The composition trap. timesTwo and plusThree are combined two different ways and applied to the same input. Predict both lines before you check.

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> timesTwo = x -> x * 2;
        Function<Integer, Integer> plusThree = x -> x + 3;

        Function<Integer, Integer> a = timesTwo.andThen(plusThree);
        Function<Integer, Integer> b = timesTwo.compose(plusThree);

        System.out.println(a.apply(4));
        System.out.println(b.apply(4));
    }
}
Exercise

What happens if you write Object o = x -> x + 1; on its own, with no other context?

Exercise

Predicate composition. Two predicates are combined and tested. Predict all three lines.

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> even = n -> n % 2 == 0;
        Predicate<Integer> positive = n -> n > 0;

        Predicate<Integer> evenAndPositive = even.and(positive);

        System.out.println(evenAndPositive.test(8));
        System.out.println(evenAndPositive.test(-4));
        System.out.println(even.negate().test(7));
    }
}

Recap

  • java.util.function supplies four workhorse interfaces: Function transforms via apply, Predicate tests via test, Consumer consumes via accept, Supplier produces via get — each defined by a single abstract method.
  • A lambda has no type of its own; it takes the type of its target, so the same body can add numbers or join text depending on context.
  • @FunctionalInterface is optional but asks the compiler to refuse a second abstract method.
  • Function.andThen(g) runs f then g; Function.compose(g) runs g then f — opposite orders, easy to invert by mistake.
  • Predicate combines with and, or, and negate.

Next you will put these interfaces to work inside the Stream API, where map takes a Function, filter takes a Predicate, and the whole pipeline runs lazily.

Checkpoint quiz

Inside java.util.function, which interface's single method returns a boolean?

Given f.andThen(g), in what order are f and g applied to the input?

Go deeper — technical resources