A functional interface is an interface with exactly one abstract method. That single rule is what makes lambdas work: when the compiler sees a lambda, it needs to know which method the lambda is implementing, and one abstract method means there is no ambiguity.
The rule, and the annotation
@FunctionalInterface
interface Validator {
boolean test(String input); // exactly one abstract method
default Validator and(Validator other) { // default methods do not count
return s -> this.test(s) && other.test(s);
}
static Validator notNull() { // static methods do not count either
return s -> s != null;
}
}
@FunctionalInterface is optional but worth adding. It makes the compiler reject a
second abstract method, so nobody can break every lambda that uses your interface by adding one
later. Without it the interface still works with lambdas — the annotation documents the intent and
enforces it.
Note what does not count against the one-method limit: default methods,
static methods, and public methods inherited from Object such as
equals or toString.
They were already there before Java 8
The idea is older than the syntax. Several interfaces you have used for years qualify, which is why they work with lambdas without anyone changing them:
Runnable job = () -> System.out.println("working"); // run()
Comparator<String> byLength = (a, b) -> a.length() - b.length(); // compare()
class Demo {
void run() {
new Thread(job).start();
List<String> names = new ArrayList<>(List.of("Christopher", "Bo"));
names.sort(byLength);
System.out.println(names); // [Bo, Christopher]
}
}
Java 8 did not invent functional interfaces. It added a compact way to implement them and a package full of ready-made ones.
The java.util.function package
Before writing your own, check whether one of these fits. These six cover almost everything:
| Interface | Method | Shape | Used for |
|---|---|---|---|
Predicate<T> | test | T → boolean | filtering, conditions |
Function<T,R> | apply | T → R | transforming |
Consumer<T> | accept | T → void | doing something with each item |
Supplier<T> | get | () → T | producing a value lazily |
UnaryOperator<T> | apply | T → T | transforming without changing type |
BiFunction<T,U,R> | apply | T, U → R | combining two things |
Predicate<String> isLong = s -> s.length() > 5;
Function<String, Integer> length = String::length;
Consumer<String> print = System.out::println;
Supplier<LocalDate> today = LocalDate::now;
UnaryOperator<String> shout = s -> s.toUpperCase() + "!";
BiFunction<Integer, Integer, Integer> add = Integer::sum;
class Demo {
void run() {
System.out.println(isLong.test("hello")); // false
System.out.println(length.apply("hello")); // 5
System.out.println(shout.apply("hey")); // HEY!
System.out.println(add.apply(20, 22)); // 42
print.accept("done");
}
}
There are primitive variants too — IntPredicate, ToIntFunction,
DoubleSupplier and a couple of dozen more. They exist to avoid boxing every value into
an Integer, which matters when you are processing millions of elements and is irrelevant
otherwise.
Composing them
This is where they stop being a curiosity. The built-in interfaces carry default
methods that build new ones out of old ones:
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> isShort = s -> s.length() < 10;
class Demo {
void run() {
Predicate<String> valid = notBlank.and(isShort);
System.out.println(valid.test("hello")); // true
System.out.println(valid.test("")); // false
System.out.println(notBlank.negate().test("")); // true
System.out.println(notBlank.or(isShort).test("")); // true
Function<String, String> trim = String::strip;
Function<String, Integer> len = String::length;
System.out.println(trim.andThen(len).apply(" hi ")); // 2
System.out.println(len.compose(trim).apply(" hi ")); // 2 — same, other order
}
}
andThen runs this one first, then the argument. compose is the reverse.
When you cannot remember which is which, use andThen — it reads left to right.
Writing your own
Write one when the built-ins do not express the intent, when you need a name that means something in your domain, or when you want more than two parameters:
@FunctionalInterface
interface Discount {
double applyTo(double price);
}
@FunctionalInterface
interface TriFunction<A, B, C, R> { // the JDK stops at two arguments
R apply(A a, B b, C c);
}
class Checkout {
double checkout(double price, Discount discount) {
return discount.applyTo(price);
}
void run() {
System.out.println(checkout(100, p -> p * 0.9)); // 90.0
System.out.println(checkout(100, p -> p - 15)); // 85.0
TriFunction<Integer, Integer, Integer, Integer> sum3 = (a, b, c) -> a + b + c;
System.out.println(sum3.apply(1, 2, 3)); // 6
}
}
Discount is the better example of the two. Function<Double,
Double> would work identically and say nothing; Discount tells the next reader
what the parameter is for, and applyTo reads better at the call site than
apply.
Where they show up
Once you recognise the shape, you see it everywhere in the standard library — and you can read those signatures instead of guessing:
List<String> names = new ArrayList<>(List.of("Ana", "Bo"));
Map<String, Integer> ages = new HashMap<>();
class Demo {
void run(List<String> names, Map<String, Integer> ages) {
names.removeIf(s -> s.isBlank()); // Predicate
names.replaceAll(String::toUpperCase); // UnaryOperator
names.forEach(System.out::println); // Consumer
ages.computeIfAbsent("Ana", k -> 0); // Function
ages.merge("Ana", 1, Integer::sum); // BiFunction
Optional.empty().orElseGet(() -> "none"); // Supplier
}
}
Two things that catch people
A lambda is not an object of the interface's "type" in any special sense. It is
an instance of some class the runtime supplies, so identity comparisons on lambdas are meaningless
and two identical lambdas are not equals:
Predicate<String> a = s -> s.isEmpty();
Predicate<String> b = s -> s.isEmpty();
class Demo {
void run(Predicate<String> a, Predicate<String> b) {
System.out.println(a == b); // false
System.out.println(a.equals(b)); // false — do not store lambdas in a Set and expect dedup
}
}
An interface with two abstract methods cannot take a lambda, and the compiler
error names the count rather than the fix. If you meant one of them to have a body, make it
default:
interface Broken {
boolean test(String s);
String describe(); // now Broken is not functional
}
@FunctionalInterface
interface Fixed {
boolean test(String s);
default String describe() { // a body, so it no longer counts
return "a test";
}
}
Next
A lambda that does nothing but call one existing method can be written more directly still. Method References is next.