What are functional interfaces in Java? Explain with examples.
java.util.function covering the most common patterns.Table of Contents
The Interview Question
"What are functional interfaces in Java? Explain with examples."
Short Answer
A functional interface is any interface that declares exactly one abstract method. It may contain any number of default and static methods. The @FunctionalInterface annotation is optional but recommended — it causes a compile-time error if you accidentally add a second abstract method.
Functional interfaces are the foundation for Java's lambda and method-reference syntax: wherever a functional interface type is expected, a lambda expression can be used directly.
Detailed Explanation
1. The @FunctionalInterface Annotation
Adding @FunctionalInterface to an interface instructs the compiler to enforce the single-abstract-method rule. Without it, the interface still qualifies as functional if it meets the criteria — the annotation just makes the intent explicit and guards against accidental breakage.
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input); // single abstract method — OK
default void log() { } // default method — allowed
static Transformer<String, Integer> length() {
return String::length; // static factory — allowed
}
}
2. Core Built-in Functional Interfaces
The java.util.function package provides general-purpose types that cover the vast majority of use cases:
| Interface | Method | Description |
|---|---|---|
Function<T,R> | R apply(T t) | Transform T → R |
BiFunction<T,U,R> | R apply(T t, U u) | Transform (T, U) → R |
Predicate<T> | boolean test(T t) | Boolean test on T |
BiPredicate<T,U> | boolean test(T t, U u) | Boolean test on two args |
Consumer<T> | void accept(T t) | Side effect on T, no return |
BiConsumer<T,U> | void accept(T t, U u) | Side effect on two args |
Supplier<T> | T get() | Produce a T, no input |
UnaryOperator<T> | T apply(T t) | Function where T in = T out |
BinaryOperator<T> | T apply(T t1, T t2) | BiFunction where all types = T |
Primitive specializations (IntFunction, ToIntFunction, IntUnaryOperator, etc.) avoid boxing overhead for int, long, and double.
3. Function Composition — andThen() and compose()
Both Function and Predicate expose methods for building composed functions:
f.andThen(g)— appliesffirst, feeds its result togf.compose(g)— appliesgfirst, feeds its result tofp1.and(p2),p1.or(p2),p1.negate()— boolean algebra on predicatesc1.andThen(c2)— chains consumers sequentially
andThen reads left-to-right: f, then g. compose reads right-to-left: g before f.Code Example
import java.util.List;
import java.util.function.*;
public class FunctionalInterfacesDemo {
public static void main(String[] args) {
// ── Function<T,R> — transform one type to another ─────────────────
Function<String, Integer> strLen = String::length;
System.out.println(strLen.apply("Techoral")); // 8
// ── Function composition: andThen vs compose ───────────────────────
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> trimThenUpper = trim.andThen(upper);
Function<String, String> upperAfterTrim = upper.compose(trim); // identical here
System.out.println(trimThenUpper.apply(" java ")); // JAVA
System.out.println(upperAfterTrim.apply(" java ")); // JAVA
// ── Predicate<T> — boolean tests and composition ──────────────────
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> startsWithJ = s -> s.startsWith("J");
Predicate<String> valid = notEmpty.and(startsWithJ);
List.of("Java", "", "Python", "JavaScript").stream()
.filter(valid)
.forEach(System.out::println); // Java, JavaScript
Predicate<Integer> isNegative = n -> n < 0;
Predicate<Integer> isPositive = isNegative.negate();
System.out.println(isPositive.test(5)); // true
System.out.println(isPositive.test(-1)); // false
// ── Consumer<T> — side effects, no return value ───────────────────
Consumer<String> print = System.out::println;
Consumer<String> shout = s -> System.out.println(s.toUpperCase());
Consumer<String> printAndShout = print.andThen(shout);
printAndShout.accept("hello"); // hello\nHELLO
// ── Supplier<T> — produce a value, no input ───────────────────────
Supplier<List<String>> listMaker = java.util.ArrayList::new;
List<String> list = listMaker.get();
list.add("item");
System.out.println(list); // [item]
// ── BiFunction<T,U,R> — two inputs, one output ────────────────────
BiFunction<String, Integer, String> repeat =
(str, times) -> str.repeat(times);
System.out.println(repeat.apply("ha", 3)); // hahaha
// ── UnaryOperator<T> — Function where input type == output type ───
UnaryOperator<String> exclaim = s -> s + "!";
System.out.println(exclaim.apply("Techoral")); // Techoral!
// ── BinaryOperator<T> — BiFunction where all types are the same ──
BinaryOperator<Integer> add = Integer::sum;
System.out.println(add.apply(3, 4)); // 7
// ── Custom @FunctionalInterface ────────────────────────────────────
Validator<String> emailCheck = s -> s.contains("@") && s.contains(".");
System.out.println(emailCheck.validate("user@techoral.com")); // true
System.out.println(emailCheck.validate("invalid-email")); // false
// ── Primitive specializations (avoid boxing) ──────────────────────
IntUnaryOperator doubler = n -> n * 2;
System.out.println(doubler.applyAsInt(21)); // 42
ToIntFunction<String> length = String::length;
System.out.println(length.applyAsInt("streams")); // 7
}
@FunctionalInterface
interface Validator<T> {
boolean validate(T value);
default Validator<T> and(Validator<T> other) {
return value -> this.validate(value) && other.validate(value);
}
}
}
When to Use
Use built-in functional interfaces when:
- Writing stream operations — almost all stream methods take a functional interface argument.
- Designing APIs that accept pluggable behaviour (strategy pattern, callbacks).
- Building composable processing pipelines via
andThen/compose.
Create a custom @FunctionalInterface when:
- The built-in ones don't match your signature (e.g., you need to throw a checked exception).
- The abstract method's name conveys important domain meaning (
validate,transform,authorize). - You want to add domain-specific default methods (e.g.,
and,negatewith domain names).
Why This Question Matters in Interviews
Functional interfaces are the backbone of Java's lambda and stream system. Interviewers verify that candidates:
- Can identify a functional interface and explain the single-abstract-method rule.
- Know the most important built-in types and their method signatures.
- Understand composition via
andThen/composeand the difference between them. - Know when to define a custom
@FunctionalInterfacevs. reusing a built-in one. - Can explain primitive specializations and why they matter for performance.
Common Pitfalls
- Adding two abstract methods — breaks the functional interface contract; the
@FunctionalInterfaceannotation will surface this as a compile error. - Confusing Function and Consumer —
Functionreturns a value;Consumerdoes not. UsingConsumerwhere aFunctionis needed causes a compile error, but mixing them mentally leads to design mistakes. - Ignoring primitive specializations — using
Function<Integer, Integer>boxes everyintinto anIntegerobject. UseIntUnaryOperatororIntFunction<R>for performance-critical code. - Forgetting that default methods count toward the interface contract — only abstract methods count toward the functional interface limit. Default and static methods do not.
- Checked exceptions in lambda bodies — standard functional interfaces don't declare checked exceptions. Wrap in a try/catch or define a custom throwing interface.
Conclusion
Functional interfaces are the glue between Java's object-oriented type system and its functional programming features. Understanding the single-abstract-method rule, the rich set of built-in types in java.util.function, and composition patterns like andThen and compose equips you to write clean, flexible, and composable Java code — and to answer this interview question thoroughly.