Functional Interface in Java – Lambdas and java.util.function
A functional interface is an interface with exactly one abstract method (a "single abstract method", or SAM, type). Because there is only one method to implement, Java lets you supply the implementation as a compact lambda expression or method reference instead of a whole class.
Functional interfaces arrived with lambdas in Java 8 and power streams, Optional, CompletableFuture and most modern Java APIs. This tutorial explains the rules, the @FunctionalInterface annotation, and the built-in interfaces in java.util.function.
1. What Makes an Interface Functional?
- It has exactly one abstract method.
- It may have any number of default and static methods.
- Abstract methods that match public methods of
Object(equals,hashCode,toString) do not count; this is whyComparator, which declaresequals, is still functional.
Classic examples existed before Java 8: Runnable (run()), Callable (call()), Comparator (compare()) and ActionListener.
2. Creating a Functional Interface
public class CustomFunctional {
@FunctionalInterface
interface Calculator {
int compute(int a, int b); // the single abstract method
default Calculator andThenDouble() { // default methods are allowed
return (a, b) -> compute(a, b) * 2;
}
static Calculator add() { return (a, b) -> a + b; } // static methods too
boolean equals(Object o); // Object methods do not count
}
public static void main(String[] args) {
// Three ways to implement it
Calculator multiply = new Calculator() { // 1. anonymous class
@Override public int compute(int a, int b) { return a * b; }
};
Calculator subtract = (a, b) -> a - b; // 2. lambda
Calculator max = Math::max; // 3. method reference
System.out.println("multiply: " + multiply.compute(6, 7));
System.out.println("subtract: " + subtract.compute(10, 4));
System.out.println("max: " + max.compute(3, 9));
System.out.println("add x2: " + Calculator.add().andThenDouble().compute(2, 3));
}
}
multiply: 42 subtract: 6 max: 9 add x2: 10
The same interface is implemented three ways. The lambda (a, b) -> a - b is converted into an object of Calculator, because the compiler knows compute is the only method it can stand for.
3. The @FunctionalInterface Annotation
@FunctionalInterface is optional, but it documents intent and makes the compiler check the rule. Adding a second abstract method then causes an error:
@FunctionalInterface
interface Validator {
boolean validate(String s);
boolean validateAll(String... s); // error: multiple non-overriding abstract methods
}
Any interface with one abstract method can be used with lambdas, annotated or not.
4. Built-in Functional Interfaces (java.util.function)
Java provides 43 ready-made functional interfaces, so you rarely need your own. The core ones:
| Interface | Method | Takes → Returns | Typical use |
|---|---|---|---|
Predicate<T> | test(T) | T → boolean | Filtering, conditions |
Function<T,R> | apply(T) | T → R | Transforming, mapping |
Consumer<T> | accept(T) | T → nothing | Printing, saving |
Supplier<T> | get() | nothing → T | Lazy values, factories |
UnaryOperator<T> | apply(T) | T → T | Modifying a value |
BinaryOperator<T> | apply(T,T) | (T, T) → T | Reducing, combining |
BiFunction<T,U,R> | apply(T,U) | (T, U) → R | Two-argument transforms |
Primitive versions such as IntPredicate, ToIntFunction, IntUnaryOperator and LongSupplier avoid boxing overhead.
import java.util.function.*;
public class BuiltInFunctional {
public static void main(String[] args) {
Predicate<String> isLong = s -> s.length() > 5;
Function<String, Integer> length = String::length;
Consumer<String> printer = s -> System.out.println(" consumed: " + s);
Supplier<String> greeting = () -> "Hello from a Supplier";
UnaryOperator<String> upper = String::toUpperCase;
BinaryOperator<Integer> sum = Integer::sum;
BiFunction<String, Integer, String> repeat = String::repeat;
System.out.println("Predicate isLong(\"Java\"): " + isLong.test("Java"));
System.out.println("Function length(\"Kafka\"): " + length.apply("Kafka"));
printer.accept("Spring Boot");
System.out.println("Supplier get(): " + greeting.get());
System.out.println("UnaryOp upper(\"java\"): " + upper.apply("java"));
System.out.println("BinaryOp sum(4, 5): " + sum.apply(4, 5));
System.out.println("BiFunction repeat(\"ab\", 3): " + repeat.apply("ab", 3));
// Primitive specialisations avoid boxing
IntPredicate even = n -> n % 2 == 0;
IntUnaryOperator square = n -> n * n;
System.out.println("IntPredicate even(10): " + even.test(10));
System.out.println("IntUnaryOperator square(12): " + square.applyAsInt(12));
}
}
Predicate isLong("Java"): false
Function length("Kafka"): 5
consumed: Spring Boot
Supplier get(): Hello from a Supplier
UnaryOp upper("java"): JAVA
BinaryOp sum(4, 5): 9
BiFunction repeat("ab", 3): ababab
IntPredicate even(10): true
IntUnaryOperator square(12): 144
5. Composing Functions and Passing Behaviour
Built-in interfaces include default methods for combining them: Predicate.and/or/negate, Function.andThen/compose and Consumer.andThen. Methods can also accept functional interfaces as parameters, passing behaviour like data:
import java.util.List;
import java.util.function.*;
public class ComposingFunctions {
public static void main(String[] args) {
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> startsWithJ = s -> s.startsWith("J");
Predicate<String> valid = notBlank.and(startsWithJ.negate()); // non-blank AND not starting with "J"
Function<String, String> trim = String::trim;
Function<String, String> clean = trim.andThen(String::toLowerCase).andThen(s -> s.replace(' ', '-'));
List<String> titles = List.of(" Spring Boot Basics ", "Java Streams", " ", "Kafka Consumers");
titles.stream()
.filter(valid)
.map(clean)
.forEach(slug -> System.out.println("slug: " + slug));
// Functional interfaces as parameters: behaviour passed like data
System.out.println("twice(+3) on 10 = " + twice(n -> n + 3, 10));
}
static int twice(IntUnaryOperator op, int value) {
return op.applyAsInt(op.applyAsInt(value));
}
}
slug: spring-boot-basics slug: kafka-consumers twice(+3) on 10 = 16
6. Rules and Notes
- A lambda's parameter and return types must match the abstract method of the target interface.
- Lambdas can use local variables only if they are final or effectively final.
- A functional interface can extend another interface if the result still has one abstract method.
- Inside a lambda,
thisrefers to the enclosing object, unlike an anonymous class. - Checked exceptions: built-in interfaces like
Functiondo not declare any, so lambdas must handle them or you define your own interface with athrowsclause.
7. Summary
- A functional interface has exactly one abstract method; default and static methods are allowed.
- It can be implemented with a lambda, a method reference or an anonymous class.
@FunctionalInterfaceasks the compiler to enforce the rule.java.util.functionprovidesPredicate,Function,Consumer,Supplierand many more.- Default methods such as
andThenandnegatelet you compose behaviour.
Related Tutorials
Frequently Asked Questions (FAQ)
Q1. What is a functional interface in Java?
An interface with exactly one abstract method, which can therefore be implemented by a lambda expression or method reference.
Q2. Is @FunctionalInterface mandatory?
No. It is optional, but it documents intent and makes the compiler report an error if the interface does not have exactly one abstract method.
Q3. Can a functional interface have default methods?
Yes, any number. Only abstract methods are counted.
Q4. Can a functional interface have static methods?
Yes, any number. Comparator.comparing and Function.identity() are examples.
Q5. Why is Comparator a functional interface if it has equals()?
Abstract methods that override public Object methods do not count, because every implementation already inherits them from Object.
Q6. Is Runnable a functional interface?
Yes. It has a single abstract method, run(), so you can write new Thread(() -> work()).
Q7. What is the difference between Function and Consumer?
Function<T,R> takes a value and returns a result. Consumer<T> takes a value and returns nothing.
Q8. What is the difference between Supplier and Consumer?
A Supplier produces a value from nothing (get()); a Consumer accepts a value and produces nothing (accept(T)).
Q9. What is a Predicate?
A functional interface whose test(T) method returns a boolean. It is used for filters and conditions and can be combined with and, or and negate.
Q10. What is the difference between UnaryOperator and Function?
UnaryOperator<T> is a Function<T,T> whose input and output types are the same.
Q11. Can a functional interface extend another interface?
Yes, as long as the total number of abstract methods remains one, for example by extending an interface and adding only default methods.
Q12. What is a SAM type?
A Single Abstract Method type, another name for a functional interface.
Q13. Why use primitive functional interfaces like IntPredicate?
They work directly with int, long or double, avoiding the cost of boxing into Integer and other wrapper objects.
Q14. How do lambdas handle checked exceptions?
A lambda can throw only the checked exceptions declared by the interface's method. With Function and similar, catch them inside the lambda or define a custom interface that declares them.
Q15. What is a method reference?
A shorthand lambda that refers to an existing method, such as String::length, System.out::println or ArrayList::new.
Q16. Can an abstract class be a target for a lambda?
No. Lambdas can only implement functional interfaces, not abstract classes, even those with one abstract method.
Q17. Is a marker interface a functional interface?
No. A marker interface has no abstract methods, while a functional interface must have exactly one.
Q18. What does Function.andThen do?
It returns a function that applies the first function and then the second to its result: f.andThen(g) means g(f(x)). compose applies them in the opposite order.
Q19. What is BiFunction?
A functional interface taking two arguments and returning a result, through apply(T t, U u).
Q20. Which package contains Java's built-in functional interfaces?
java.util.function, introduced in Java 8. Others, such as Runnable, Callable and Comparator, live in java.lang, java.util.concurrent and java.util.