What are functional interfaces in Java? Explain with examples.

Java code
Published on May 31, 2026
#Java#InterviewQuestions#FunctionalInterfaces
Quick Answer: A functional interface has exactly one abstract method and is the target type for a lambda or method reference; Java ships a rich set in java.util.function covering the most common patterns.

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:

InterfaceMethodDescription
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) — applies f first, feeds its result to g
  • f.compose(g) — applies g first, feeds its result to f
  • p1.and(p2), p1.or(p2), p1.negate() — boolean algebra on predicates
  • c1.andThen(c2) — chains consumers sequentially
Memory aid: 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, negate with 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 / compose and the difference between them.
  • Know when to define a custom @FunctionalInterface vs. 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 @FunctionalInterface annotation will surface this as a compile error.
  • Confusing Function and ConsumerFunction returns a value; Consumer does not. Using Consumer where a Function is needed causes a compile error, but mixing them mentally leads to design mistakes.
  • Ignoring primitive specializations — using Function<Integer, Integer> boxes every int into an Integer object. Use IntUnaryOperator or IntFunction<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.