Back to Functional Interfaces
Q1Question 1 of 13

What is a functional interface?

Key Concepts
  • A functional interface has exactly one abstract method (SAM: Single Abstract Method) and can be the target of a lambda expression.
  • @FunctionalInterface is optional but strongly recommended: it makes the compiler verify exactly one abstract method exists.
  • Default methods and static methods do not count toward the abstract method limit because they have implementations.
  • Methods inherited from Object (toString, equals, hashCode) do not count toward the SAM limit either.
  • The four core functional interfaces are Predicate (T to boolean), Function (T to R), Consumer (T to void), and Supplier (nothing to T).
  • Functional interfaces support composition: Predicate has and/or/negate, Function has andThen/compose, Consumer has andThen.
  • A functional interface can extend another interface as long as the combined result still has exactly one abstract method.

Definition

A functional interface has exactly one abstract method (SAM), can have unlimited default and static methods, and is the target type for lambda expressions and method references.
  • SAM rule: a functional interface has exactly ONE abstract method. Default methods (with body) and static methods do NOT count.
  • @FunctionalInterface is an optional compile-time annotation that prevents accidentally adding a second abstract method.
  • Predicate takes T and returns boolean: used for filtering and condition checks.
  • Function takes T and returns R: used for transformation and mapping.
  • Consumer takes T and returns void: used for side effects like logging and notifications.
  • Supplier takes nothing and returns T: used for lazy creation and factory patterns.
  • Composition methods like and(), andThen(), and negate() let you build complex behaviour from simple building blocks.

A functional interface is like a bank form with exactly one blank to fill in. The Predicate form has one blank: "Is this account active? YES or NO." The Function form has one blank: "Convert this account to a receipt." The Consumer form has one blank: "Process this account (no response needed)." You can add printed instructions (default methods) and official stamps (static methods) to the form, but there is only one blank for the customer to fill in. @FunctionalInterface is the quality control stamp that verifies exactly one blank exists.

Understanding the Concepts

What is a functional interface?

A functional interface has exactly one abstract method. It is the type that lambda expressions and method references target. The annotation @FunctionalInterface is optional but strongly recommended because it provides compile-time verification.

What are the rules for counting abstract methods?

JAVA
package com.bank.core;

import java.math.BigDecimal;

@FunctionalInterface
interface AccountValidator {
    boolean validate(String accountId, BigDecimal balance); // ONE abstract method

    // Default: has body, does NOT count
    default AccountValidator and(AccountValidator other) {
        return (id, bal) -> this.validate(id, bal) && other.validate(id, bal);
    }

    // Static: has body, does NOT count
    static AccountValidator alwaysValid() {
        return (id, bal) -> true;
    }

    // Object method: does NOT count
    String toString();
}
JAVA
package com.bank.core;

@FunctionalInterface
interface TransferProcessor {
    void process(String fromAccount, String toAccount);
    // void audit(String fromAccount, String toAccount);
    // COMPILE ERROR! @FunctionalInterface with 2 abstract methods!
}
// Without @FunctionalInterface, adding audit() silently breaks
// all lambda usage sites across the codebase.

What are the four core functional interfaces?

DIAGRAM
| Interface | Signature | Purpose | Banking Example |
|-----------|-----------|---------|----------------|
| Predicate<T> | boolean test(T t) | Filtering | `account -> account.isActive()` |
| Function<T,R> | R apply(T t) | Transformation | `account -> account.getBalance()` |
| Consumer<T> | void accept(T t) | Side effect | `account -> auditLog.save(account)` |
| Supplier<T> | T get() | Factory/lazy creation | `() -> UUID.randomUUID().toString()` |
JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.function.*;
import java.util.List;

public class CoreFunctionalDemo {
    record Account(String id, String holder, BigDecimal balance, boolean active) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("SAV2001", "Rajesh", new BigDecimal("125000"), true),
            new Account("SAV2002", "Priya", new BigDecimal("8000"), false),
            new Account("CUR3001", "Amit", new BigDecimal("250000"), true)
        );

        Predicate<Account> isActive = Account::active;
        Predicate<Account> isLarge = a -> a.balance().compareTo(new BigDecimal("100000")) > 0;
        Function<Account, String> toSummary = a -> a.id() + ": Rs." + a.balance();
        Consumer<Account> logAccount = a -> System.out.println("Processed: " + a.id());
        Supplier<String> txnId = () -> "TXN-" + System.currentTimeMillis();

        // Composition
        accounts.stream()
            .filter(isActive.and(isLarge))
            .map(toSummary)
            .forEach(System.out::println);

        System.out.println("Generated ID: " + txnId.get());
    }
}
TEXT
SAV2001: Rs.125000
CUR3001: Rs.250000
Generated ID: TXN-1705312800000

How does composition work?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.function.*;

public class CompositionDemo {
    public static void main(String[] args) {
        // Predicate composition
        Predicate<BigDecimal> positive = amt -> amt.signum() > 0;
        Predicate<BigDecimal> large = amt -> amt.compareTo(new BigDecimal("100000")) > 0;
        Predicate<BigDecimal> positiveLarge = positive.and(large);
        Predicate<BigDecimal> notPositive = positive.negate();

        // Function composition
        Function<String, BigDecimal> parse = BigDecimal::new;
        Function<BigDecimal, String> format = amt -> "Rs." + amt.toPlainString();
        Function<String, String> parseAndFormat = parse.andThen(format);

        System.out.println(parseAndFormat.apply("50000")); // Rs.50000

        // Consumer composition
        Consumer<String> log = msg -> System.out.println("LOG: " + msg);
        Consumer<String> audit = msg -> System.out.println("AUDIT: " + msg);
        Consumer<String> both = log.andThen(audit);

        both.accept("Transfer completed");
    }
}
TEXT
Rs.50000
LOG: Transfer completed
AUDIT: Transfer completed

Can a functional interface extend another interface?

Yes, as long as the combined result still has exactly one abstract method. If the parent has an abstract method, the child must override it as a default method.

JAVA
package com.bank.core;

interface Describable { String describe(); } // 1 abstract method

// WRONG: 2 abstract methods (execute + describe)
// @FunctionalInterface
// interface AccountAction extends Describable { void execute(); }

// CORRECT: override parent abstract as default
@FunctionalInterface
interface AccountAction extends Describable {
    void execute(); // 1 abstract method
    @Override
    default String describe() { return "Account action"; } // parent becomes default
}

What are the extended family interfaces?

BiFunction takes two inputs and returns one output. UnaryOperator extends Function where input and output are the same type. BinaryOperator extends BiFunction where all three types are the same, used in reduce operations. BiPredicate takes two inputs and returns boolean.

What should a senior engineer know?

The seven core interfaces (Predicate, Function, Consumer, Supplier, UnaryOperator, BinaryOperator, BiFunction) plus their Bi variants cover all common lambda shapes. Creating custom functional interfaces is appropriate only when the domain name adds clarity, like AccountValidator being more readable than BiPredicate. Named predicates stored as static factory methods in a validation rules class are reusable across different streams and independently testable.

Visual Overview

DIAGRAM
  Four Core Functional Interfaces:
  ─────────────────────────────────
  Predicate<T>   T ──→ boolean     filter(), removeIf()
  Function<T,R>  T ──→ R           map(), computeIfAbsent()
  Consumer<T>    T ──→ void        forEach(), ifPresent()
  Supplier<T>   () ──→ T           orElseGet(), generate()

  Composition:
  ─────────────
  Predicate:  .and()  .or()  .negate()  Predicate.not()
  Function:   .andThen()  .compose()  Function.identity()
  Consumer:   .andThen()
  Supplier:   (no composition)

  Extended Family:
  ────────────────
  BiFunction<T,U,R>     (T, U) ──→ R
  UnaryOperator<T>      T ──→ T      (extends Function<T,T>)
  BinaryOperator<T>     (T, T) ──→ T (extends BiFunction<T,T,T>)

Key Takeaway

A functional interface has exactly one abstract method. The @FunctionalInterface annotation is optional but recommended because it prevents accidentally adding a second abstract method. Default methods and static methods do not count toward the abstract method limit because they have implementations. This means Predicate can have one abstract method (test) plus three default methods (and, or, negate) plus one static method (not) and still be a valid functional interface.

The four core functional interfaces in java.util.function cover all common lambda shapes. Predicate takes T and returns boolean for filtering. Function takes T and returns R for transformation. Consumer takes T and returns void for side effects. Supplier takes nothing and returns T for lazy creation. Each supports composition: Predicate has and, or, and negate for combining conditions. Function has andThen and compose for chaining transformations. Consumer has andThen for sequential actions.

In Rajesh's account validation service at National Bank of India, a custom AccountValidator functional interface has one abstract method (validate), default composition methods (and, or), and static factory methods (hasMinBalance, isActive). Each factory returns a lambda implementing the validate method. This replaces separate validator classes with one-line lambdas that compose fluently.

Remember one sentence: a functional interface has exactly one abstract method, lambdas implement it, and @FunctionalInterface is the compile-time guard.

---

Share
Use ← → arrow keys to navigate