Java Functional Interfaces Explained with Examples

A functional interface is a Java interface with exactly one abstract method after inherited methods and special Object-method rules are considered.

Lesson 81 of 127Advanced13 min read

A functional interface is a Java interface with exactly one abstract method after inherited methods and special Object-method rules are considered. It gives a lambda or method reference a precise contract. Learning the standard interfaces helps you choose a suitable type instead of inventing a new interface for every small action. Predicate tests, Function transforms, Consumer performs an action, and Supplier produces a value without receiving an argument. Each has a different purpose.

The one abstract method rule

An interface may contain default methods and static methods and still be functional, because those methods already have implementations. The single abstract method is the behavior a lambda supplies. The @FunctionalInterface annotation asks the compiler to verify this intention. It is optional for using lambdas, but valuable when maintaining your own interface: adding a second unrelated abstract method will then fail immediately instead of silently breaking its intended use.

Predicate describes a yes or no test

Predicate<T> declares boolean test(T value). Use it for eligibility checks, validation rules, or filtering. A predicate should communicate what true means, such as available or withinBudget. Predicates can be combined using and, or, and negate. The combination follows boolean short-circuit behavior: with and, a false first test means the second does not run. Prefer tests without side effects so their meaning does not depend on the chosen combination.

Function converts one value into another

Function<T, R> declares R apply(T value). The input and result types can differ: Function<String, Integer> can return a string length. Its andThen method runs a second function on the first result, while compose applies another function before the current one. Follow the types when reading a composition. If the first function returns Integer, the next must accept that result or a compatible type; a mismatched chain is rejected at compile time.

Consumer and Supplier have different directions

Consumer<T> accepts a value and returns void, making it appropriate for displaying a message or adding a value to a destination. Supplier<T> receives no argument and returns a value, making it useful for deferred construction or obtaining a fresh object. A supplier does not guarantee freshness by itself: its body decides whether to create something or return an existing reference. These interfaces describe signatures, not promises about side effects, caching, or performance.

Primitive and two-argument variants

Using Function<Integer, Integer> involves boxed Integer values. For numeric work, IntUnaryOperator takes an int and returns an int without that boxing contract. IntPredicate and IntConsumer offer similar primitive specializations. BiFunction<T, U, R> receives two inputs, such as a price and quantity. Choose a standard interface that matches the actual inputs and output, rather than selecting Function simply because the operation is called a function in ordinary conversation.

Naming a domain-specific interface

A custom functional interface is useful when its method name and documentation express a business rule more clearly than apply. The FareRule interface below receives a distance and returns a fare. Its name also provides a place to document units and allowed values. The lambda still implements only one operation. Avoid creating dozens of nearly identical interfaces that add no meaning; standard interfaces remain easier to recognize when the domain contract is already obvious.

Practice tasks and checks

Create a Function<String, String> that trims input and a Predicate<String> that rejects an empty result. Test them separately with " Oak " and a string containing only spaces before combining them in application code. Next, create a Supplier<ArrayList<String>> and call get twice; add a label to one list and verify the other remains empty. This checks the difference between producing a new object and returning a shared object. For numeric practice, replace a Function<Integer, Integer> with IntUnaryOperator and call applyAsInt. Explain which signature changed and why a Consumer cannot replace either calculation when the caller needs a result. Choose interfaces by their contracts instead of by how similar their names sound.

Java Summary

  • Choose functional interfaces by their input and result signatures.
  • Default methods do not count as abstract functional methods.
  • Composition must preserve compatible intermediate types.
  • Continue with method references when these examples are clear.

Java Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Combining validation rules

Save this program as VisitorRules.java. Compile it with javac VisitorRules.java and run it with java VisitorRules.

Expected output:

Mina passes the length test and then the uppercase test, producing true. The empty string fails the length test. Because and short-circuits, charAt is never called for that empty input, avoiding an index exception. This example intentionally orders the checks to establish the second check's precondition. If null is an accepted input, add a separate non-null predicate before these two; the current contract expects an actual string.

Java
import java.util.function.Predicate;
public class VisitorRules {
    public static void main(String[] args) {
        Predicate<String> longEnough = name -> name.length() >= 3;
        Predicate<String> startsCapital = name -> Character.isUpperCase(name.charAt(0));
        Predicate<String> valid = longEnough.and(startsCapital);
        System.out.println(valid.test("Mina"));
        System.out.println(valid.test(""));
    }
}
Output
true
false
Example 2

Composing transformations

Save this program as ReadingTransform.java. Compile it with javac ReadingTransform.java and run it with java ReadingTransform.

Expected output:

The pipeline first parses the text fourteen into an Integer. The second function doubles that integer, giving twenty-eight. andThen describes the execution order explicitly. Invalid numeric text would cause NumberFormatException during parsing; function composition does not automatically turn that exception into a default value. Put validation or recovery where the application's input contract requires it, rather than assuming every standard functional interface handles errors.

Java
import java.util.function.Function;
public class ReadingTransform {
    public static void main(String[] args) {
        Function<String, Integer> parse = Integer::parseInt;
        Function<Integer, Integer> doubleValue = value -> value * 2;
        Function<String, Integer> pipeline = parse.andThen(doubleValue);
        System.out.println(pipeline.apply("14"));
    }
}
Output
28
Example 3

Defining a meaningful custom contract

Save this program as FareCalculation.java. Compile it with javac FareCalculation.java and run it with java FareCalculation.

Expected output:

The only abstract method is centsFor, so FareRule is functional even though it also has a default method. For three kilometers, the lambda adds a base charge of two hundred fifty cents to three charges of eighty cents, producing four hundred ninety. Calling unit executes the default implementation, which the lambda does not replace. Keeping the result in cents avoids decimal rounding concerns in this small calculation.

Java
public class FareCalculation {
    @FunctionalInterface
    interface FareRule {
        int centsFor(int kilometers);
        default String unit() { return "cents"; }
    }
    public static void main(String[] args) {
        FareRule localTrip = kilometers -> 250 + kilometers * 80;
        System.out.println(localTrip.centsFor(3));
        System.out.println(localTrip.unit());
    }
}
Output
490
cents

Java FAQ

Is Runnable a functional interface?

Yes. Its abstract run method accepts no arguments and returns void. A lambda assigned to Runnable therefore uses empty parentheses and an action body, such as () -> System.out.println("Ready"). Invoking run directly does not create a new thread.

Does @FunctionalInterface make an interface functional?

The method structure determines whether it is functional. The annotation verifies that structure and records your intention. It cannot make an interface with two unrelated abstract methods accept a lambda, and compilation will report the invalid annotation.

Which functional interface should I use?

Match the shape of the job. A yes-or-no test is a Predicate<T>, converting one value into another is a Function<T, R>, using a value without returning anything is a Consumer<T>, producing a value from nothing is a Supplier<T>, and running an action with no input or output is a Runnable.

What is the difference between Function and UnaryOperator?

UnaryOperator<T> is a Function<T, T>: the input and output have the same type, as in trimming a string or doubling a number. BinaryOperator<T> is the matching two-input version, which reduce uses. They exist mainly to make declarations shorter and the intent clearer.

What are BiFunction and BiConsumer for?

They take two inputs instead of one. A BiFunction<String, Integer, String> might combine a name and a quantity into a label, and Map.forEach takes a BiConsumer that receives each key and value. Java has no built-in three-argument versions; define your own interface when you need one.

Java Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Why does VisitorRules safely test an empty string?

Multiple choicemedium

Which interface produces a value without receiving an input?

Java Quiz

2 questions to check what you learned.

  1. What is the only behavior the FareRule lambda replaces?

    • centsFor
    • unit
    • Both methods
  2. Which type avoids the Integer boxing contract for int-to-int calculations?

    • IntUnaryOperator
    • Function<Object, Object>
    • Consumer<Integer>