library
intermediateFunctional Interfaces & @FunctionalInterface
Understand the core functional interfaces (Predicate, Function, Consumer, Supplier) and create custom ones.
A functional interface is one with a single abstract method, which is what makes it a target for a lambda. Everything else about the topic follows from counting abstract methods correctly.
Functional interface = a standard electrical outlet shape (the method signature). Lambda = any appliance that fits the outlet. Predicate is a 'yes/no' outlet, Function is a 'convert' outlet, Consumer is a 'use up' outlet.
Key Concepts
1
The counting rule has exceptions worth knowing. Default and static methods do not count, because they
have bodies. Methods that redeclare a public method of java.lang.Object — equals, hashCode,
toString — do not count either, since every implementation inherits one. That is why Comparator is a
functional interface despite declaring both compare and equals, which is a favourite interview
question.
publicjava.lang.ObjectequalshashCodetoString
2
@FunctionalInterface is a compile-time assertion, not a requirement. A lambda works on any interface
with one abstract method, annotated or not. What the annotation buys is a compile error the moment someone
adds a second abstract method, which protects every caller who wrote a lambda against it. Put it on
interfaces you intend to be lambda targets; its absence changes nothing at runtime.
@FunctionalInterface
3
The java.util.function families are worth memorising as shapes. Function<T,R> transforms,
Predicate<T> tests, Consumer<T> accepts and returns nothing, Supplier<T> produces from nothing,
UnaryOperator<T> is a Function whose input and output types match, and BiFunction, BiPredicate and
BiConsumer take two arguments. Knowing these by shape means you rarely need to define your own.
java.util.functionFunction<T,R>Predicate<T>Consumer<T>Supplier<T>
4
Primitive specialisations exist to avoid boxing, and in hot loops that matters more than it looks.
IntPredicate avoids wrapping every int in an Integer; ToIntFunction<T> avoids wrapping the result.
Each boxed value is an allocation, so a stream over millions of primitives can spend more time in the
allocator than in your logic.
IntPredicateintIntegerToIntFunction<T>
5
Checked exceptions are the real friction. None of the standard functional interfaces declare checked
exceptions, so a lambda that does IO will not compile inside map without a try/catch. The options are
wrapping in an unchecked exception, writing your own interface that declares the checked type, or handling
it inside the lambda. There is no elegant answer, and saying that plainly is better than pretending
otherwise.
map
6
What the interviewer is probing.1. "What makes an interface functional?" Probing: the rule and its exceptions. Stalls: "One
method." Moves up: one abstract method, not counting defaults, statics, or redeclared Object methods.
Object
7
2. "Is @FunctionalInterface required?" Probing: annotation semantics. Stalls: "Yes, for lambdas."
Moves up: no — it is a compile-time check that stops someone adding a second abstract method later.
@FunctionalInterface
8
3. "Why is Comparator functional when it declares equals?" Probing: the Object exception. Stalls:
"It isn't." Moves up: equals is a public Object method, so it does not count toward the total.
Objectequals
9
4. "Which interface fits a method that takes a String and returns a boolean?" Probing: fluency with
the shapes. Stalls: "A custom interface." Moves up: Predicate<String>, or Function<String,Boolean>
if boxing is acceptable — and why Predicate is better.
Predicate<String>Function<String,Boolean>Predicate
10
5. "How do you handle a checked exception in a lambda?" Probing: honesty. Stalls: "Just throw it."
Moves up: the standard interfaces do not declare them; wrap, define your own throwing interface, or
handle it inside — each with a cost.
11
The core shapes12
Function<T,R> T -> R apply
BiFunction<T,U,R> (T,U) -> R apply
UnaryOperator<T> T -> T apply
BinaryOperator<T> (T,T) -> T apply
Predicate<T> T -> boolean test
BiPredicate<T,U> (T,U) -> boolean test
Consumer<T> T -> void accept
BiConsumer<T,U> (T,U) -> void accept
Supplier<T> () -> T get
Runnable () -> void run
Callable<V> () -> V (throws) call13
Primitive specialisations (no boxing)14
IntPredicate int -> boolean
IntFunction<R> int -> R
ToIntFunction<T> T -> int
IntUnaryOperator int -> int
IntBinaryOperator (int,int) -> int
IntSupplier () -> int
IntConsumer int -> void
... and Long/Double equivalents15
Why boxing matters in a loop
Integer per element = 16 B header + 4 B value = 16-24 B
1,000,000 elements = ~20 MB of garbage, plus the indirection
IntStream over int[] = zero allocations16
The counting rule, applied17
interface Comparator<T> {
int compare(T a, T b); // abstract -> counts
boolean equals(Object o); // Object method -> does NOT
default Comparator<T> reversed() { ... } // default -> no
static <T> Comparator<T> naturalOrder() { } // static -> no
}
Total abstract: 1 -> functional