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
applyinFunctionortestinPredicate. Calling it runs the lambda's body. - Supplier / Consumer
- A
Supplierproduces a value from nothing (get()). AConsumertakes a value and produces nothing (accept(t)), so it's used for side effects like printing. - Function / Predicate
- A
Functionturns an input into an output (apply). APredicateanswers yes or no about an input (test). - Primitive specialisation
- A version of a functional interface that works directly on
int,longordouble, such asIntPredicate, so no wrapper objects are created. - Boxing
- Automatically wrapping a primitive like
intin an object likeIntegerso 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.
@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.
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.
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.
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=805Primitive 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.
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.
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.
Try it yourself
- 1
Swap andThen and compose
In the composition example, predict
times3.andThen(plus2).apply(5)andtimes3.compose(plus2).apply(5)before running. Then add both lines and check. - 2
Find the boxing boundary
In the boxing example, change the inputs from
10to11and then12. Predictsame objectfor each (the squares are 121 and 144), then run. Which side of 127 is each result on? - 3
Break the annotation
Add a second abstract method
String name();toPriceRule. Predict the compiler error and which line it points at. Then turn it intodefault String name() { return "rule"; }and see the error disappear.
Code & diagrams
Expected output
Supplier -> Namaste
Consumer -> NAMASTE!
Function -> 7
Predicate -> false
UnaryOperator -> haha
BinaryOperator -> 9
BiFunction -> abababString::isEmpty is a method reference, a shorter way to write s -> s.isEmpty() (Topic 10.3). Predicate.not arrived in Java 11.
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]Every boxed result outside -128..127 is a new Integer object. The Int* interfaces never create one.
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 trueInside the default method then, the lambda's apply(p) calls the enclosing PriceRule's own apply: this in a lambda is the enclosing object.
Expected output
1000 after rules = 850.0
parsed: [4, 8]
failed on -3: negativeBreak 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); }.
Break #2
Throw a checked exception from a stream lambda
Write names.stream().map(n -> Files.readString(Path.of(n))).toList();.
Break #3
Use a primitive as a type argument
Write Predicate<int> even = n -> n % 2 == 0;.
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, soexecutor.submit(() -> compute())pickssubmit(Callable)whencompute()returns a value, while() -> { compute(); }pickssubmit(Runnable). Ambiguities between functional overloads are why many APIs name methods differently (comparingIntvscomparingLong). - ▸
The JDK limited specialisations to
int,longanddoubleto avoid a combinatorial explosion of interfaces (already 43). Project Valhalla aims to makeFunction<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::isBlankisn't Java. It's a static factory, soPredicate.not(String::isBlank)reads naturally with a static import.
Remember this
- 1
The rule is precise: a functional interface has exactly one abstract method (its functional method, or SAM: single abstract method).
default,staticandprivatemethods don't count, because they already have bodies. Abstract methods that just redeclare a publicObjectmethod, likeequals(Object)inComparator, don't count either, since every object already implements them. - 2
@FunctionalInterfaceis an optional annotation that asks the compiler to check the rule. Interfaces likeRunnable,CallableandComparatorexisted 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
java.util.functiondefines 43 standard interfaces in four families: **Supplier<T>** (nothing in,Tout:get()), **Consumer<T>** (Tin, nothing out:accept), **Function<T, R>** (Tin,Rout:apply) and **Predicate<T>** (Tin,booleanout:test). Two-argument versions addBi(BiFunction<T, U, R>,BiConsumer,BiPredicate), and **UnaryOperator<T>andBinaryOperator<T>** are functions whose inputs and output share one type. - 4
Generics only work with objects, so
Function<Integer, Integer>boxes everyintinto anInteger(Topic 8.6). The primitive specialisations avoid that:IntPredicate,IntUnaryOperator,IntBinaryOperator,IntSupplier,ToIntFunction<T>,IntFunction<R>,ObjIntConsumer<T>, and the same forlonganddouble. The naming is a pattern:IntXtakes anint,ToIntXreturns one. - 5
The interfaces come with default methods for building bigger functions from small ones:
f.andThen(g)(f first, then g) andf.compose(g)(g first) onFunction;and,or,negateonPredicate, plusPredicate.not(...)(Java 11) andPredicate.isEqual(x);andThenonConsumer;Function.identity(). - 6
None of the standard functional methods declare
throws, so a lambda for them can't throw a checked exception likeIOException(Topic 7.3). Either catch it inside the lambda (often rethrowing asUncheckedIOException), or declare your own functional interface whose method saysthrows Exception. Write your own interface too when a domain name reads better (PriceRuleinstead ofUnaryOperator<Double>) or when you need three or more parameters, since there's noTriFunction.
Explain it without notes
What exactly qualifies an interface as a functional interface?
Name the four main families in java.util.function and their method names.
Why do primitive specialisations like IntPredicate exist?
How do you deal with checked exceptions inside lambdas?
Practice
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.
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}.
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,doubleand 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 realthrowsclause 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,negateandPredicate.not.Done when you can handle checked exceptions in lambdas in at least two ways.