All topics
library
intermediate

Functional 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 shapes
12
  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)  call
13
  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 equivalents
15
  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 allocations
16
  The counting rule, applied
17
    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