A functional interface in Java is an interface that has a single abstract method, so a lambda expression or a method reference can provide its implementation. Default methods, static methods and public methods inherited from Object do not count toward that single method.
We use functional interfaces whenever we pass behavior as a value, for example a filter condition to a stream, a sort order to List.sort(), a task to an ExecutorService or a pricing rule to a checkout service. The JDK ships 43 ready-made ones in java.util.function, and we write our own when a domain name reads better.
The following example implements five JDK functional interfaces with lambdas and method references and calls each one.
Function<Integer, Integer> square = x -> x * x;
int squared = square.apply(5); // 25
Predicate<String> isBlank = String::isBlank;
boolean blank = isBlank.test(" "); // true
Supplier<List<String>> newCart = ArrayList::new;
List<String> cart = newCart.get(); // []
Consumer<String> addToCart = cart::add;
addToCart.accept("pizza");
int items = cart.size(); // 1
BinaryOperator<Integer> add = Integer::sum;
int total = add.apply(120, 80); // 200
Notice that each variable has a functional interface type, and each call uses that interface’s one abstract method, namely apply(), test(), get() or accept(). We go through the single-method rule and the @FunctionalInterface annotation, the JDK interface families, composing them, writing generic ones, and handling checked exceptions, and we finish with a checkout example. The interfaces are the building blocks of the Stream API.
1. What is a Functional Interface?
A functional interface is a contract for one piece of behavior. Because the compiler knows the only abstract method, it can match a lambda to that method by its parameter and return types, and this target type is what gives a lambda its meaning. The single-method rule is part of the Java Language Specification.

Interfaces with one abstract method existed long before lambdas. Runnable (Java 1.0) and Comparator (Java 1.2) were functional in shape, and Java 8 added lambda expressions, method references, the @FunctionalInterface annotation and the java.util.function package. Old code that passed an anonymous class to new Thread() can pass a lambda today without any change to the interface.
1.1. Only One abstract Method Is Allowed
A functional interface is also called a Single Abstract Method interface (SAM interface). Besides the single abstract method, it can declare any number of methods that do not count.
- Methods with a body declared as default methods, which implementations inherit.
- Static methods, such as Comparator.naturalOrder() or Predicate.not().
- Abstract methods that match a public method of Object, such as equals() or toString(), because every implementation already has them.
The following example is a Discount interface for a food delivery app. It has one abstract method and a default method that chains two discounts.
@FunctionalInterface
interface Discount {
int apply(int price);
default Discount then(Discount next) {
return price -> next.apply(apply(price));
}
static Discount none() {
return price -> price;
}
}
Discount tenOff = price -> price - 10;
Discount half = price -> price / 2;
int once = tenOff.apply(250); // 240
int chained = tenOff.then(half).apply(250); // 120
int unchanged = Discount.none().apply(250); // 250
1.2. Implemented by Lambda Expressions
A lambda expression or a method reference creates an instance of a functional interface. The Comparator interface is a good case to learn from, because its source declares two abstract methods, compare(T, T) and equals(Object). The equals() method matches a public method of Object, so it does not count, and Comparator stays a functional interface.
List<String> dishes = new ArrayList<>(List.of("pizza", "tea", "noodles"));
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
int cmp = byLength.compare("tea", "pizza"); // -1
dishes.sort(byLength.thenComparing(Comparator.reverseOrder()));
List<String> sorted = dishes; // [tea, pizza, noodles]
A lambda can only target an interface. An abstract class with one abstract method is not a functional interface, so we must subclass it the old way.
2. @FunctionalInterface Annotation
The @FunctionalInterface annotation tells the compiler that we mean the interface to be functional. If the interface has zero or more than one abstract method, the compiler reports an error.
The annotation is optional, because any interface with one abstract method is a functional interface, but adding it stops a teammate from breaking every lambda by adding a second abstract method later. The JDK puts it on Runnable, Callable, Comparator and every interface in java.util.function.
The following interface has a second abstract method, so it fails to compile.
// does not compile: two abstract methods in a @FunctionalInterface
@FunctionalInterface
interface PriceRule {
double apply(double price);
double describe(String text);
}
PriceRule.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
PriceRule is not a functional interface
multiple non-overriding abstract methods found in interface PriceRule
Without the annotation, the same interface compiles, and the error appears instead at every place where a lambda is assigned to it. The annotation moves the error to the interface itself, where the cause is.
3. Functional Interfaces in JDK
The java.util.function package has 43 interfaces, but they follow four basic shapes. Once we know the shape, the name of any variant tells us what it takes and returns.
| Interface | Method | Takes | Returns | Typical use |
|---|---|---|---|---|
| Supplier<T> | get() | nothing | T | Lazy defaults, factories, orElseGet() |
| Consumer<T> | accept(T) | T | nothing | forEach(), logging, sending |
| BiConsumer<T, U> | accept(T, U) | T, U | nothing | Map.forEach() |
| Predicate<T> | test(T) | T | boolean | filter(), removeIf() |
| BiPredicate<T, U> | test(T, U) | T, U | boolean | Checks on two values |
| Function<T, R> | apply(T) | T | R | map(), converters |
| BiFunction<T, U, R> | apply(T, U) | T, U | R | Map.merge(), replaceAll() |
| UnaryOperator<T> | apply(T) | T | T | List.replaceAll() |
| BinaryOperator<T> | apply(T, T) | T, T | T | reduce() |
Every row is in use in the following snippet, which manages a small food order.
List<String> cart = new ArrayList<>();
BiConsumer<List<String>, String> addTo = List::add;
addTo.accept(cart, "pizza");
addTo.accept(cart, "salad");
List<String> items = cart; // [pizza, salad]
BiPredicate<String, Integer> longerThan = (s, n) -> s.length() > n;
boolean longName = longerThan.test("pizza", 3); // true
Function<String, Integer> nameLength = String::length;
int length = nameLength.apply("salad"); // 5
BiFunction<Integer, Integer, Integer> lineTotal = (price, qty) -> price * qty;
int line = lineTotal.apply(120, 3); // 360
UnaryOperator<String> shout = String::toUpperCase;
String upper = shout.apply("pizza"); // "PIZZA"
BinaryOperator<Integer> cheaper = BinaryOperator.minBy(Comparator.naturalOrder());
int cheapest = cheaper.apply(120, 90); // 90
3.1. Primitive Specializations
Generics work only with objects, so a Function<Integer, Integer> boxes every int into an Integer. The primitive versions avoid that cost, and their method names end with the return type, such as applyAsInt() or getAsDouble(). The primitive streams such as IntStream take these interfaces.
IntPredicate isEven = n -> n % 2 == 0;
boolean even = isEven.test(4); // true
ToIntFunction<String> letters = String::length;
int count = letters.applyAsInt("noodles"); // 7
IntBinaryOperator plus = Integer::sum;
int sum = plus.applyAsInt(2, 3); // 5
IntFunction<String> label = n -> "Table " + n;
String table = label.apply(4); // "Table 4"
3.2. Older Interfaces That Are Functional
Several interfaces outside java.util.function also qualify, and we pass lambdas to them every day.
- Runnable has run(), which takes nothing and returns nothing. We pass it to Thread and ExecutorService.execute().
- Callable<V> has call(), which returns a value and may throw a checked exception. We pass it to ExecutorService.submit().
- Comparator<T> has compare() and sorts collections and streams.
- Comparable<T> has one abstract method too, but a class implements it to define its own natural order, so we never write it as a lambda. See Comparable.
4. Composing Functional Interfaces
Many JDK functional interfaces have default methods that combine two instances into a new one. Composition lets us build a complex rule from small, named rules, which are easier to test one by one.
Function<Integer, Integer> addDelivery = p -> p + 40;
Function<Integer, Integer> halfPrice = p -> p / 2;
int deliveryFirst = addDelivery.andThen(halfPrice).apply(200); // 120
int halfFirst = addDelivery.compose(halfPrice).apply(200); // 140
Predicate<String> isVeg = dish -> dish.startsWith("veg");
Predicate<String> isSpicy = dish -> dish.contains("spicy");
boolean mildVeg = isVeg.and(isSpicy.negate()).test("veg curry"); // true
boolean either = isVeg.or(isSpicy).test("spicy wings"); // true
boolean notVeg = Predicate.not(isVeg).test("chicken"); // true
The method andThen() runs the current function first and the argument second, while compose() runs the argument first. The results differ, 120 against 140, so the order is part of the business rule. More predicate examples are in chaining predicates.
5. Generic Functional Interfaces
A functional interface can declare type parameters like any other generic interface. The caller picks the type when declaring the variable, so one interface works for numbers, strings or our own records.
@FunctionalInterface
interface Combiner<T> {
T combine(T first, T second);
}
Combiner<Integer> multiply = (a, b) -> a * b;
int product = multiply.combine(2, 3); // 6
Combiner<String> join = (a, b) -> a + " " + b;
String text = join.combine("Hello", "World"); // "Hello World"
Notice that Combiner<T> has the same shape as the JDK’s BinaryOperator<T>. We write our own generic interface when the domain name helps readers, and use the JDK one otherwise, because JDK interfaces come with composition methods and every library accepts them.
5.1. Restricting the Type With extends
A bounded type parameter such as T extends Number limits which types the caller may use, and it lets the lambda call methods of the bound, such as doubleValue().
@FunctionalInterface
interface NumberCombiner<T extends Number> {
T combine(T first, T second);
}
NumberCombiner<Double> times = (a, b) -> a * b;
double area = times.combine(4.0, 6.0); // 24.0
NumberCombiner<Integer> larger = (a, b) -> a.doubleValue() >= b.doubleValue() ? a : b;
int max = larger.combine(7, 3); // 7
// does not compile: String is not within the bound Number
NumberCombiner<String> joined = (a, b) -> a + b;
5.2. Specializing a Generic Interface
An interface that extends Combiner<Integer> fixes the type argument, so the new interface is no longer generic. It inherits the single abstract method, which makes it a functional interface as well.
@FunctionalInterface
interface IntCombiner extends Combiner<Integer> {
}
IntCombiner addInts = Integer::sum;
int nine = addInts.combine(4, 5); // 9
5.3. A Generic Method Cannot Be a Lambda Target
A type parameter on the interface is fine, but a type parameter on the method itself is not. When the single abstract method is generic, a lambda cannot implement it, because a lambda has no syntax to declare type parameters. A method reference to a generic method works.
interface Identity {
<T> T same(T value);
}
// does not compile: invalid functional descriptor, method (T)T is generic
Identity lambda = value -> value;
Identity ref = Objects::requireNonNull;
String tea = ref.same("tea"); // "tea"
6. Functional Interfaces and Checked Exceptions
None of the methods in java.util.function declares a checked exception, so a lambda passed to map() cannot call a method that throws IOException or URISyntaxException without a try-catch. A generic functional interface with an exception type parameter solves that once for the whole codebase.
@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R, ?> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception e) {
throw new IllegalArgumentException("Cannot convert " + t, e);
}
};
}
Function<String, URI> toUri = unchecked(URI::new);
URI menu = toUri.apply("https://example.com/menu"); // https://example.com/menu
URI bad = toUri.apply("not a uri"); // IllegalArgumentException: Cannot convert not a uri
The wrapper keeps the original exception as the cause, so the stack trace still shows the real problem. Handling exceptions in streams compares this wrapper with returning an Optional or a result record.
7. Pricing Rules in a Checkout Service
A food delivery app applies several rules at checkout, such as a coupon, a member discount and a delivery fee for small orders. Product managers change these rules often, so the checkout code keeps each rule as a small function and chains them in a fixed order.
record Order(int subtotal, boolean member) {}
Order order = new Order(600, true);
Predicate<Order> isMember = Order::member;
Predicate<Order> bigOrder = o -> o.subtotal() >= 500;
boolean memberDeal = isMember.and(bigOrder).test(order); // true
UnaryOperator<Integer> coupon = p -> p - 50;
UnaryOperator<Integer> memberOff = p -> p * 90 / 100;
UnaryOperator<Integer> deliveryFee = p -> p < 500 ? p + 40 : p;
Function<Integer, Integer> pricing = coupon.andThen(memberOff).andThen(deliveryFee);
int toPay = pricing.apply(order.subtotal()); // 535
The order of 600 drops to 550 with the coupon and to 495 with the member discount, so the delivery fee applies and the customer pays 535. Swapping the rules changes the amount, which is why the chain lives in one place and each rule gets its own unit test. Spring apps often register such rules as beans and inject them as a List<UnaryOperator<Integer>>.
8. Functional Interface in Java FAQs
Most questions about our own functional interfaces come down to what counts toward the single abstract method.
8.1. Can a Functional Interface Have Multiple Methods?
Yes, as long as only one of them is abstract. It can have any number of default and static methods, plus abstract methods that match public Object methods, such as equals().
8.2. Is @FunctionalInterface Mandatory?
No. Any interface with one abstract method works as a lambda target. The annotation only adds a compile-time check, and we use it on every interface meant for lambdas.
8.3. Can a Functional Interface Extend Another Interface?
Yes. It stays functional if the result still has one abstract method, either inherited or declared, as IntCombiner shows in section 5. Extending two interfaces whose methods have the same signature also works, because the two methods merge into one.
8.4. Is Runnable a Functional Interface?
Yes. Runnable has one abstract method, run(), and is marked with @FunctionalInterface since Java 8, so new Thread(() -> sendEmail()) compiles.
8.5. What Is the Difference Between Function and UnaryOperator?
A UnaryOperator<T> is a Function<T, T> whose input and output types are the same. It extends Function, so it has the same apply() method and can be passed wherever a matching Function is expected.
8.6. Why Can a Lambda Use Only Effectively Final Variables?
The lambda may run later or on another thread, so Java copies the values of captured local variables into it. Allowing changes to those locals would make the copy and the original disagree, so the compiler rejects any local variable that is assigned more than once.
9. Conclusion
A functional interface has one abstract method, and that single method is what lets a lambda or a method reference implement it. Default methods, static methods and Object methods do not change that, and @FunctionalInterface makes the compiler guard the rule.
For most code, the JDK families of Supplier, Consumer, Predicate and Function, with their primitive and two-argument variants, are enough. Our own interfaces earn their place when a domain name reads better, when we need a bounded generic type, or when the method must declare a checked exception.
10. References
- JLS 25, section 9.8 Functional Interfaces
- JLS 25, section 15.27.3 Type of a Lambda Expression
- FunctionalInterface Javadoc
- java.util.function package summary
- Using the Functional Interfaces on dev.java
Happy Learning !!
thank you…somehow it helped me to gather info on functional interface
Thanks for the details
# It will really help readers if we can add more scenarios when this particular feature can/should be used
# Which current features it can replace or act as an alternative to
# What would be the BIG-O or any performance metric we can back up with
Very Nice Post
Do a Functional Interface can extends another Functional or Non Functional Interface?
Any behavioral change?
No A Functional Interface can not extends another Funtional Interface as the child funtional interface will be having two abstract method which will give exception.
Excellent post. Kudos to the creator!
This site has very useful content very easy to understand.
Fantastic Post regarding Functional interface
Hi Lokesh,
A few things to possibly add are below are list of FunctionalInterface in jdk1.8 libraries
https://stackoverflow.com/questions/27743315/a-summary-of-the-parameters-and-return-type-of-functional-interfaces-in-the-pack/28162720#28162720
Thanks…it clears the confusion of which are functional interface .
Thanks, nice post
I have doubt . What is the main purpose of implementing functional interfaces in java8 ?
Thanks to you lokesh, now I know about functional interfaces but dont know when to use it ?
“The type in which lambda expressions are converted, are always of functional interface type”. You would like to read more in this section: https://howtodoinjava.com/java8/lambda-expressions/#functional_interface
Nice one.This is absolutely clear to understand.
But only one doubt in which scenario we will use Functional Interface/SAM Interface ?
Thanks & Regards
Tony
Whenever you are planning to write a method where you want to pass a lambda expression as argument.