Command Palette

Search for a command to run...

PHASE 10Intermediate Java 8+ ~32 min· topic 2 of 8

Topic 10.2

Functional Interfaces

In one line

A functional interface is an interface with exactly one abstract method, and it is the only kind of type a lambda can become. java.util.function provides the standard shapes (Supplier, Consumer, Function, Predicate and friends), plus primitive versions that avoid boxing.

Think of it like this

A wall socket. The socket doesn't care whether you plug in a lamp, a kettle or a phone charger; it only cares that the plug has the right shape. A functional interface is a socket shape for code: "takes a String, returns a boolean". Any lambda with that shape plugs in, whatever it does inside.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Abstract method
A method declared without a body. A class implementing the interface must supply the body.
SAM
Single Abstract Method. Another name for a functional interface's one abstract method.
Functional method
The one abstract method of a functional interface, such as apply in Function or test in Predicate. Calling it runs the lambda's body.
Supplier / Consumer
A Supplier produces a value from nothing (get()). A Consumer takes a value and produces nothing (accept(t)), so it's used for side effects like printing.
Function / Predicate
A Function turns an input into an output (apply). A Predicate answers yes or no about an input (test).
Primitive specialisation
A version of a functional interface that works directly on int, long or double, such as IntPredicate, so no wrapper objects are created.
Boxing
Automatically wrapping a primitive like int in an object like Integer so it can be used where an object is required.
Composition
Building a new function by joining existing ones, like "first add 2, then multiply by 3".

Step by step

01What makes an interface functional

Count only the abstract methods that aren't public Object methods. Comparator<T> declares compare and equals abstractly, plus dozens of default and static methods, yet it's functional: equals is already implemented by every object, so only compare needs a lambda.

Inheritance counts too: an interface that extends a functional interface and adds no abstract method is still functional (UnaryOperator<T> extends Function<T, T>). One that adds a second abstract method is not.

Main.javawhole filejava
@FunctionalInterface
interface Validator {
    boolean isValid(String input);             // the one abstract method

    default Validator and(Validator other) {   // has a body: doesn't count
        return s -> isValid(s) && other.isValid(s);
    }

    static Validator notBlank() {              // static: doesn't count
        return s -> !s.trim().isEmpty();
    }

    boolean equals(Object o);                  // public Object method: doesn't count
}

02@FunctionalInterface makes the compiler check

Put @FunctionalInterface on an interface and add a second abstract method: the compiler refuses, and tells you exactly why. Without the annotation the interface compiles fine, and the error only appears where someone tries to use a lambda for it, far away from the real cause.

terminal
$ javac Main.java
── expected output ──
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Calculator is not a functional interface
multiple non-overriding abstract methods found in interface Calculator
1 error

03The four families

Almost every lambda you write fits one of four shapes. Learn them by what goes in and what comes out, and you can read any stream pipeline: filter takes a Predicate, map a Function, forEach a Consumer, Stream.generate a Supplier.

The four familiesdiagram
Rendering diagram…

04Operators and two-argument versions

UnaryOperator<T> is a Function<T, T>: same type in and out (s -> s.trim()). BinaryOperator<T> is a BiFunction<T, T, T>: two of a type in, one out (Integer::sum). Reductions like reduce (Topic 10.5) take a BinaryOperator.

BiFunction<T, U, R>, BiConsumer<T, U> and BiPredicate<T, U> take two arguments. Map.forEach takes a BiConsumer<K, V>, and Map.merge takes a BiFunction. There's no three-argument version in the JDK.

Main.javawhole filejava
UnaryOperator<String> trim = s -> s.trim();
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);   // String.repeat: Java 11
BiConsumer<String, Integer> show = (k, v) -> System.out.println(k + "=" + v);

Map<String, Integer> stock = new TreeMap<>(Map.of("tea", 3, "milk", 1));
stock.forEach(show);                     // Map.forEach takes a BiConsumer
stock.merge("tea", 5, sum);              // Map.merge takes a BiFunction: tea=8

05Primitive specialisations avoid boxing

Function<Integer, Integer> square = n -> n * n; unboxes the argument, multiplies, then boxes the result into a new Integer (only values from -128 to 127 come from a cache). In a loop over millions of numbers that's millions of small objects for the garbage collector.

IntUnaryOperator square = n -> n * n; works on raw ints, with no objects at all. The JDK provides specialisations only for int, long and double (plus BooleanSupplier); for char, short or float you use the int/double versions.

Main.javawhole filejava
IntPredicate        isOdd  = n -> n % 2 != 0;        // int -> boolean
IntUnaryOperator    square = n -> n * n;            // int -> int
IntBinaryOperator   add    = (a, b) -> a + b;       // int, int -> int
ToIntFunction<String> len  = s -> s.length();       // String -> int
IntFunction<String> label  = n -> "#" + n;          // int -> String
ObjIntConsumer<StringBuilder> pad = (sb, n) -> sb.append(" ".repeat(n));

06Composing with default methods

andThen and compose both chain two functions; they differ in order. f.andThen(g) means "f, then g" (g(f(x))); f.compose(g) means "g, then f" (f(g(x))). Predicate combines with and, or and negate, which short-circuit like && and ||.

These are ordinary default methods that return new lambdas. Nothing is evaluated when you compose; the work happens when you call apply or test on the result.

Composing with default methodsdiagram
Rendering diagram…

07Checked exceptions don't fit the standard shapes

Function.apply doesn't declare throws IOException, so a lambda used as a Function can't throw it either. The error points at the call inside the lambda.

Choices: catch inside the lambda and rethrow new UncheckedIOException(e); write a helper that wraps a throwing lambda (see the last example); or declare your own interface with throws. Don't swallow the exception and return null: that just moves the failure somewhere harder to find.

terminal
$ javac Main.java
── expected output ──
Main.java:8: error: unreported exception IOException; must be caught or declared to be thrown
names.stream().map(n -> Files.readString(Path.of(n))).toList();
^
1 error

Try it yourself

  1. 1

    Swap andThen and compose

    In the composition example, predict times3.andThen(plus2).apply(5) and times3.compose(plus2).apply(5) before running. Then add both lines and check.

  2. 2

    Find the boxing boundary

    In the boxing example, change the inputs from 10 to 11 and then 12. Predict same object for each (the squares are 121 and 144), then run. Which side of 127 is each result on?

  3. 3

    Break the annotation

    Add a second abstract method String name(); to PriceRule. Predict the compiler error and which line it points at. Then turn it into default String name() { return "rule"; } and see the error disappear.

Code & diagrams

The core shapes of java.util.function Java 8+ New tab
Sign in to run this example in your browser.

Expected output

Supplier       -> Namaste
Consumer       -> NAMASTE!
Function       -> 7
Predicate      -> false
UnaryOperator  -> haha
BinaryOperator -> 9
BiFunction     -> ababab
Composing functions, predicates and consumers Java 11+ New tab

String::isEmpty is a method reference, a shorter way to write s -> s.isEmpty() (Topic 10.3). Predicate.not arrived in Java 11.

Sign in to run this example in your browser.

Expected output

plus2.andThen(times3).apply(5) = 21
plus2.compose(times3).apply(5) = 17
identity: same
long AND starts with a: [apple, avocado]
long OR starts with a:  [apple, ant, banana, avocado]
NOT long:               [ant, fig]
not(isEmpty):           [x, y]
print order-1
log: [order-1]
Primitive specialisations and the cost of boxing Java 8+ New tab

Every boxed result outside -128..127 is a new Integer object. The Int* interfaces never create one.

Sign in to run this example in your browser.

Expected output

sum of odd squares 1..5 = 35
length of 'stream' = 6
10000 boxed twice: equals true, same object false
100 boxed twice: same object true
Your own functional interfaces, including one that throws Java 8+ New tab

Inside the default method then, the lambda's apply(p) calls the enclosing PriceRule's own apply: this in a lambda is the enclosing object.

Sign in to run this example in your browser.

Expected output

1000 after rules = 850.0
parsed: [4, 8]
failed on -3: negative

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Two abstract methods under @FunctionalInterface

Declare @FunctionalInterface interface Calculator { int add(int a, int b); int subtract(int a, int b); }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Calculator is not a functional interface
multiple non-overriding abstract methods found in interface Calculator
1 error

Break #2

Throw a checked exception from a stream lambda

Write names.stream().map(n -> Files.readString(Path.of(n))).toList();.

terminal
$ javac Main.java
── what you'll see ──
Main.java:8: error: unreported exception IOException; must be caught or declared to be thrown
names.stream().map(n -> Files.readString(Path.of(n))).toList();
^
1 error

Break #3

Use a primitive as a type argument

Write Predicate<int> even = n -> n % 2 == 0;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: unexpected type
Predicate<int> even = n -> n % 2 == 0;
^
required: reference
found: int
1 error

Myth vs fact

Myth

An interface must have @FunctionalInterface to be used with lambdas.

Fact

Any interface with exactly one abstract method works. The annotation only adds a compile-time check and documentation.

Myth

A functional interface can only have one method.

Fact

It can have any number of default, static and private methods, and redeclare Object methods like equals. Only one method may be abstract.

Myth

Two functional interfaces with the same shape are interchangeable.

Fact

They're unrelated types. A Supplier<String> can't be assigned to a Callable<String> even though both are () -> String. Convert with a method reference: Callable<String> c = supplier::get;.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    JLS 9.8 defines the function type of an interface by merging inherited abstract methods with override-equivalent signatures. So interface A { void run(); } interface B { void run(); } interface C extends A, B {} is still functional. A generic functional method (<T> T make()) can't be implemented by a lambda, only by a method reference, because lambdas can't declare type parameters.

  • ▸

    Overload resolution with lambdas uses shape: an expression lambda like () -> compute() is both void-compatible and value-compatible, so executor.submit(() -> compute()) picks submit(Callable) when compute() returns a value, while () -> { compute(); } picks submit(Runnable). Ambiguities between functional overloads are why many APIs name methods differently (comparingInt vs comparingLong).

  • ▸

    The JDK limited specialisations to int, long and double to avoid a combinatorial explosion of interfaces (already 43). Project Valhalla aims to make Function<int, int>-style generics possible, which would make most of them unnecessary.

  • ▸

    Predicate.not (Java 11) exists because method references can't be negated: !String::isBlank isn't Java. It's a static factory, so Predicate.not(String::isBlank) reads naturally with a static import.

Remember this

  1. 1

    The rule is precise: a functional interface has exactly one abstract method (its functional method, or SAM: single abstract method). default, static and private methods don't count, because they already have bodies. Abstract methods that just redeclare a public Object method, like equals(Object) in Comparator, don't count either, since every object already implements them.

  2. 2

    @FunctionalInterface is an optional annotation that asks the compiler to check the rule. Interfaces like Runnable, Callable and Comparator existed long before Java 8 and became lambda targets automatically; the annotation just documents the intent and stops someone accidentally adding a second abstract method later.

  3. 3

    java.util.function defines 43 standard interfaces in four families: **Supplier<T>** (nothing in, T out: get()), **Consumer<T>** (T in, nothing out: accept), **Function<T, R>** (T in, R out: apply) and **Predicate<T>** (T in, boolean out: test). Two-argument versions add Bi (BiFunction<T, U, R>, BiConsumer, BiPredicate), and **UnaryOperator<T> and BinaryOperator<T>** are functions whose inputs and output share one type.

  4. 4

    Generics only work with objects, so Function<Integer, Integer> boxes every int into an Integer (Topic 8.6). The primitive specialisations avoid that: IntPredicate, IntUnaryOperator, IntBinaryOperator, IntSupplier, ToIntFunction<T>, IntFunction<R>, ObjIntConsumer<T>, and the same for long and double. The naming is a pattern: IntX takes an int, ToIntX returns one.

  5. 5

    The interfaces come with default methods for building bigger functions from small ones: f.andThen(g) (f first, then g) and f.compose(g) (g first) on Function; and, or, negate on Predicate, plus Predicate.not(...) (Java 11) and Predicate.isEqual(x); andThen on Consumer; Function.identity().

  6. 6

    None of the standard functional methods declare throws, so a lambda for them can't throw a checked exception like IOException (Topic 7.3). Either catch it inside the lambda (often rethrowing as UncheckedIOException), or declare your own functional interface whose method says throws Exception. Write your own interface too when a domain name reads better (PriceRule instead of UnaryOperator<Double>) or when you need three or more parameters, since there's no TriFunction.

Explain it without notes

01

What exactly qualifies an interface as a functional interface?

02

Name the four main families in java.util.function and their method names.

03

Why do primitive specialisations like IntPredicate exist?

04

How do you deal with checked exceptions inside lambdas?

Practice

01

Build a Function<String, String> called clean by composing String::trim and String::toLowerCase with andThen, then apply it to " HeLLo World " and print the result in square brackets.

02

Write a method static List<Integer> filter(int[] nums, IntPredicate rule) and use it with a predicate for "even and greater than 10" built from two IntPredicates and and. Use {4, 12, 15, 20, 7}.

03

Declare @FunctionalInterface interface TriFunction<A, B, C, R> { R apply(A a, B b, C c); } and use it to compute a price from quantity, unit price and a discount percentage: 3, 250.0, 10 should give 675.0.

Trade-offs

  • ↔

    Standard interfaces (Function, Predicate) make APIs instantly familiar and composable; custom interfaces (PriceRule, RetryPolicy) carry domain meaning and can declare checked exceptions, but need their own composition methods.

  • ↔

    Primitive specialisations avoid boxing but multiply API surface: a library that wants to support int, long, double and objects needs four overloads, and overloads with lambda arguments easily become ambiguous.

  • ↔

    Wrapping checked exceptions in unchecked ones keeps lambdas concise but hides the failure from the method signature; callers must know to catch UncheckedIOException. For code where I/O failure is a normal outcome, a loop with a real throws clause can be clearer.

Done when you can

  • Done when you can state the exact rule for a functional interface, including the Object-method exception.

  • Done when you can pick the right standard interface for a given input/output shape without looking it up.

  • Done when you choose IntPredicate-style specialisations where boxing would hurt.

  • Done when you can compose functions and predicates with andThen, compose, and, or, negate and Predicate.not.

  • Done when you can handle checked exceptions in lambdas in at least two ways.