Every lambda you write needs a type, and that type is always a functional interface. Master the four core ones — Predicate, Function, Consumer, Supplier — and stream signatures like filter(Predicate<? super T>) stop looking like noise and start telling you exactly what to pass. This page gives you the definition, the big four with runnable code, the composition methods interviewers probe, and the rules for writing your own.
What makes an interface "functional"
A functional interface has exactly one abstract method (often abbreviated SAM — Single Abstract Method). That single method is what a lambda expression implements.
Three things do not count against the limit:
defaultmethods (they have bodies),staticmethods (they have bodies),- abstract methods that match
publicmethods ofObject, likeboolean equals(Object).
That last rule is why Comparator is functional even though it declares both compare and equals.
@FunctionalInterface
interface EmiCalculator {
double emi(double principal, int months); // the one abstract method
default double totalPayable(double principal, int months) {
return emi(principal, months) * months;
}
}
public class SamDemo {
public static void main(String[] args) {
EmiCalculator flat = (p, m) -> (p + p * 0.10) / m;
System.out.println(flat.emi(120_000, 12)); // 11000.0
System.out.println(flat.totalPayable(120_000, 12)); // 132000.0
}
}
The @FunctionalInterface annotation is optional but valuable: it makes the compiler reject any future second abstract method, protecting every lambda already written against the interface. This is abstraction at its purest — one behaviour, no ceremony.
The big four in java.util.function
| Interface | Abstract method | Input → Output | Powers |
|---|---|---|---|
Predicate<T> |
boolean test(T t) |
T → boolean | filter, removeIf |
Function<T,R> |
R apply(T t) |
T → R | map, computeIfAbsent |
Consumer<T> |
void accept(T t) |
T → void | forEach, peek |
Supplier<T> |
T get() |
() → T | orElseGet, lazy init |
Memorise the mnemonic by role: Predicate tests, Function transforms, Consumer uses, Supplier provides.
Predicate: yes/no questions
import java.util.List;
import java.util.function.Predicate;
public class PredicateDemo {
record Candidate(String name, int marks, boolean hasProject) {}
public static void main(String[] args) {
Predicate<Candidate> passed = c -> c.marks() >= 60;
Predicate<Candidate> projectReady = Candidate::hasProject;
// Compose: both conditions must hold
Predicate<Candidate> interviewReady = passed.and(projectReady);
List<Candidate> batch = List.of(
new Candidate("Sneha", 72, true),
new Candidate("Vikram", 55, true),
new Candidate("Rahul", 81, false)
);
batch.stream()
.filter(interviewReady)
.forEach(c -> System.out.println(c.name())); // Sneha
}
}
Notice and() — Predicate ships with and, or, negate and the static Predicate.not(). Composing small named predicates beats one giant boolean expression: each piece is testable and reusable.
Function: transformations
import java.util.function.Function;
public class FunctionDemo {
public static void main(String[] args) {
Function<String, String> trim = String::trim;
Function<String, Integer> length = String::length;
// andThen: trim first, then measure
Function<String, Integer> trimmedLength = trim.andThen(length);
// compose: measure the UNtrimmed string? No — compose runs the
// argument first: length.compose(trim) == trim.andThen(length)
System.out.println(trimmedLength.apply(" Hyderabad ")); // 9
}
}
f.andThen(g) runs f then g; f.compose(g) runs g then f. Interviewers ask for the difference constantly, and mixing them up in a pipeline produces wrong results, not errors — so anchor it now: andThen = left to right.
Consumer: side effects
Consumer<T> accepts a value and returns nothing — printing, saving, publishing. forEach takes one, and andThen chains two consumers to run in sequence over the same element:
Consumer<String> log = s -> System.out.println("LOG: " + s);
Consumer<String> audit = s -> System.out.println("AUDIT: " + s);
List.of("enrolled", "paid").forEach(log.andThen(audit));
Supplier: deferred creation
Supplier<T> takes nothing and produces a value only when get() is called. That deferral is the whole point:
Optional<Config> cached = Optional.empty();
Config cfg = cached.orElseGet(() -> loadFromDisk()); // runs only if empty
With orElse(loadFromDisk()), the load would execute even when the Optional has a value. The orElse vs orElseGet distinction is covered further in Java Optional.
Common mistake: A common mistake beginners make is treating
Function<T, Boolean>andPredicate<T>as interchangeable. They are different types with different methods (applyreturns a boxedBoolean,testreturns primitiveboolean), andfilteraccepts only Predicate. Choose Predicate whenever the answer is yes/no.
Beyond the big four: variants you will actually meet
- Bi-versions:
BiPredicate<T,U>,BiFunction<T,U,R>,BiConsumer<T,U>take two arguments.Map.forEach((key, value) -> ...)uses BiConsumer. - Operators:
UnaryOperator<T>is aFunction<T,T>(used byList.replaceAll);BinaryOperator<T>is aBiFunction<T,T,T>(used byreduce). - Primitive specializations:
IntPredicate,IntFunction<R>,ToIntFunction<T>,IntSupplierand friends avoid boxing on hot paths.IntStream.map(IntUnaryOperator)exists precisely so a million ints are not wrapped into a million Integers. - Old friends that qualify:
Runnable(() → void),Callable<V>(() → V, throws),Comparator<T>— all pre-Java-8 interfaces that happen to have one abstract method, so lambdas work with them everywhere.
Pro tip: Before defining a custom functional interface, scan
java.util.function— with 43 interfaces, the shape you need almost always exists. A custom interface is justified mainly when you need a domain name for readability (likeEmiCalculatorabove) or a checked exception in the signature.
How they plug into streams
Read a stream pipeline and you can now name every parameter type:
list.stream()
.filter(c -> c.marks() >= 60) // Predicate<Candidate>
.map(Candidate::name) // Function<Candidate,String>
.sorted(String::compareTo) // Comparator<String>
.forEach(System.out::println); // Consumer<String>
This is why we insist you learn functional interfaces before streams: once you can label each slot, stream documentation reads itself, and compiler errors about mismatched lambdas point straight at the fix.
Interview note: A standard rapid-fire round: "Name the functional interface for () → T. For T → boolean. For (T, U) → R." Practise answering with the interface and its method — "Supplier, get()", "Predicate, test()", "BiFunction, apply()". Following up abstract-method names separates candidates who used these from those who memorised a table.
Writing your own functional interface
Do it when the domain deserves a name or the built-ins cannot express the signature (checked exceptions, three-plus parameters):
@FunctionalInterface
interface RowMapper<R> {
R map(java.sql.ResultSet rs, int rowNum) throws java.sql.SQLException;
}
This mirrors the design Spring uses for JDBC row mapping: two parameters plus a checked SQLException, which no java.util.function type allows. Keep custom interfaces generic where practical, annotate them, and give the method a verb name.
Two design guidelines when you publish methods that accept functional interfaces:
- Widen with wildcards. Accept
Predicate<? super T>rather thanPredicate<T>, exactly as the JDK does inStream.filter. APredicate<Object>should be usable wherever aPredicate<String>is expected. - Avoid ambiguous overloads. If a class offers both
run(Runnable r)andrun(Callable<Integer> c), the callrun(() -> 1)may force callers into ugly casts because the compiler cannot pick a target type. Give the overloads different names instead — this is whyExecutorServicehasexecuteandsubmitbehaving differently, and it is a real API-design question at senior interviews.
Lambdas, anonymous classes and functional interfaces together
A functional interface does not require a lambda — all three of these produce a valid Predicate<String>:
// 1. Lambda
Predicate<String> p1 = s -> s.isEmpty();
// 2. Method reference
Predicate<String> p2 = String::isEmpty;
// 3. Anonymous class (pre-Java-8 style, still legal)
Predicate<String> p3 = new Predicate<String>() {
@Override public boolean test(String s) { return s.isEmpty(); }
};
All three are interchangeable at the call site. Reach for the anonymous class only when you genuinely need instance state or a self-referencing this; otherwise the lambda or method reference is shorter and clearer. Understanding that the interface is the contract and the lambda is merely one way to satisfy it is the key conceptual step — it explains why decade-old APIs like Collections.sort accepted lambdas on day one of Java 8 without any change.
Where to go next
You now hold the vocabulary that the entire Java 8 ecosystem is written in. Next, put it to work in Java Streams, where all four interfaces appear in every pipeline, then review the broader Java 8 features guide to see how the pieces connect. In our Java Full Stack course, this topic is a single evening of drills — big four, composition methods, one custom interface — and it pays back in every stream exercise that follows.
Frequently Asked Questions
What is a functional interface in Java?
Is the @FunctionalInterface annotation mandatory?
What is the difference between Predicate and Function in Java?
When would I use Supplier instead of passing a value directly?
What are BiFunction and BiPredicate?
Can a functional interface have default and static methods?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

