Functional Interfaces
A functional interface is simply an interface that has exactly one abstract method. That single-method contract is what lets Java treat a lambda expression or a method reference as an instance of the interface — no anonymous class boilerplate required.
What Makes an Interface “Functional”?
The rule is strict: one abstract method, no more. An interface can still have any number of default or static methods and remain functional. Java 8 introduced the @FunctionalInterface annotation to make the contract explicit and compiler-enforced.
@FunctionalInterface
interface Greeter {
String greet(String name); // the one abstract method
}
If you accidentally add a second abstract method, the compiler immediately rejects the annotation:
@FunctionalInterface
interface Broken {
void doA();
void doB(); // compile error: multiple non-overriding abstract methods
}
Tip: Always add
@FunctionalInterfaceto your own single-method interfaces. It’s free documentation and a compile-time safety net.
Using a Functional Interface with a Lambda
Before lambdas, you’d implement a single-method interface with an anonymous class. Lambdas collapse that into one line:
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
public class FunctionalDemo {
public static void main(String[] args) {
MathOperation add = (a, b) -> a + b;
MathOperation multiply = (a, b) -> a * b;
System.out.println("5 + 3 = " + add.operate(5, 3));
System.out.println("5 * 3 = " + multiply.operate(5, 3));
}
}
Output:
5 + 3 = 8
5 * 3 = 15
The lambda (a, b) -> a + b is just a concise implementation of MathOperation. The compiler infers the target type from the variable declaration.
Built-In Functional Interfaces (java.util.function)
Java 8 ships a rich set of ready-to-use functional interfaces in the java.util.function package so you rarely need to write your own. They fall into four families:
| Interface | Signature | Purpose |
|---|---|---|
Predicate<T> | boolean test(T t) | Test a condition |
Function<T, R> | R apply(T t) | Transform T into R |
Consumer<T> | void accept(T t) | Consume a value (side-effect) |
Supplier<T> | T get() | Produce a value |
BiFunction<T,U,R> | R apply(T t, U u) | Transform two inputs into one output |
UnaryOperator<T> | T apply(T t) | Function where input and output are the same type |
BinaryOperator<T> | T apply(T t1, T t2) | BiFunction where all types are the same |
Predicate — testing a condition
import java.util.function.Predicate;
public class PredicateDemo {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
// combine predicates
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isPositiveEven = isEven.and(isPositive);
System.out.println(isPositiveEven.test(6)); // true
System.out.println(isPositiveEven.test(-2)); // false
}
}
Output:
true
false
true
false
Predicate is used heavily in Stream operations like .filter().
Function — transforming a value
import java.util.function.Function;
public class FunctionDemo {
public static void main(String[] args) {
Function<String, Integer> strLength = s -> s.length();
System.out.println(strLength.apply("Hello")); // 5
System.out.println(strLength.apply("DevCraftly")); // 10
// chain with andThen
Function<Integer, String> intToLabel = n -> "Length: " + n;
Function<String, String> lengthLabel = strLength.andThen(intToLabel);
System.out.println(lengthLabel.apply("Java")); // Length: 4
}
}
Output:
5
10
Length: 4
Consumer — performing side effects
import java.util.function.Consumer;
import java.util.List;
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
List<String> names = List.of("alice", "bob", "carol");
names.forEach(printUpper);
}
}
Output:
ALICE
BOB
CAROL
Supplier — producing a value lazily
import java.util.function.Supplier;
import java.time.LocalDate;
public class SupplierDemo {
public static void main(String[] args) {
Supplier<LocalDate> today = () -> LocalDate.now();
System.out.println("Today is: " + today.get());
}
}
Output:
Today is: 2026-06-13
Supplier is perfect for lazy initialization — the lambda body runs only when get() is called, not when the variable is assigned.
Primitive Specializations
Boxing a primitive like int into Integer every time you pass it through a Function<Integer, Integer> wastes memory and CPU. Java provides primitive specializations to avoid that overhead:
| Generic | Primitive equivalent |
|---|---|
Function<Integer, Integer> | IntUnaryOperator |
Predicate<Integer> | IntPredicate |
Consumer<Integer> | IntConsumer |
Supplier<Integer> | IntSupplier |
Function<T, Integer> | ToIntFunction<T> |
import java.util.function.IntPredicate;
public class PrimitiveDemo {
public static void main(String[] args) {
IntPredicate isOdd = n -> n % 2 != 0;
System.out.println(isOdd.test(9)); // true
System.out.println(isOdd.test(8)); // false
}
}
Tip: Prefer primitive specializations in hot loops or large stream pipelines — they eliminate autoboxing entirely.
Composing Functions
The Function, Predicate, and Consumer interfaces expose default methods for building pipelines without nesting:
import java.util.function.Function;
public class ComposeDemo {
public static void main(String[] args) {
Function<Integer, Integer> doubleIt = x -> x * 2;
Function<Integer, Integer> addThree = x -> x + 3;
// andThen: doubleIt first, then addThree
Function<Integer, Integer> doubleThenAdd = doubleIt.andThen(addThree);
System.out.println(doubleThenAdd.apply(5)); // (5*2)+3 = 13
// compose: addThree first, then doubleIt
Function<Integer, Integer> addThenDouble = doubleIt.compose(addThree);
System.out.println(addThenDouble.apply(5)); // (5+3)*2 = 16
}
}
Output:
13
16
andThen(g)— applythis, thengcompose(g)— applygfirst, thenthis
Writing Your Own Functional Interface
Sometimes the built-in interfaces don’t match your domain. Writing a custom one is trivial:
@FunctionalInterface
interface Transformer<T> {
T transform(T input);
// default helper — still functional (only one abstract method)
default Transformer<T> andThen(Transformer<T> after) {
return input -> after.transform(this.transform(input));
}
}
public class CustomFIDemo {
public static void main(String[] args) {
Transformer<String> trim = String::trim;
Transformer<String> upper = String::toUpperCase;
Transformer<String> pipeline = trim.andThen(upper);
System.out.println(pipeline.transform(" hello world "));
}
}
Output:
HELLO WORLD
Notice how method references like String::trim satisfy the interface because trim() matches the T transform(T input) signature.
Under the Hood
How the JVM represents lambdas
When the compiler sees a lambda, it does not generate a new .class file the way an anonymous class would. Instead it uses the invokedynamic bytecode instruction (introduced in Java 7) together with LambdaMetafactory. At runtime, the JVM creates a class on the fly — once, on first call — and caches it. Subsequent calls reuse the same instance when the lambda captures no state (non-capturing lambdas). This makes lambdas substantially cheaper than old-style anonymous classes.
Target typing
The compiler resolves which functional interface a lambda should become based on context — the variable type, method parameter type, or cast. This is called target typing. A single lambda body x -> x * 2 can become an IntUnaryOperator, a Function<Integer, Integer>, or any other compatible functional interface depending on where it appears.
@FunctionalInterface at the bytecode level
The annotation itself has @Retention(RUNTIME), so it is also visible via reflection. Frameworks can query method.getAnnotation(FunctionalInterface.class) to verify a type at runtime. The JDK’s own java.util.function types all carry it.
Note:
Runnable,Callable<V>,Comparator<T>, andActionListenerall predate Java 8 but are valid functional interfaces — they each have exactly one abstract method and work seamlessly with lambdas.
Quick Reference
| Task | Interface to reach for |
|---|---|
| Filter a list | Predicate<T> |
| Map one type to another | Function<T, R> |
| Print / log / save a value | Consumer<T> |
| Generate / lazy-load a value | Supplier<T> |
| Combine two values into one | BiFunction<T,U,R> |
| Sort objects | Comparator<T> |
| Run a task in a thread | Runnable |
| Return a result from a thread | Callable<V> |
Related Topics
- Lambda Expressions — the syntax that makes functional interfaces so concise
- Method References — an even shorter way to satisfy a functional interface
- Stream API — where
Predicate,Function, andConsumerare used most heavily - Stream Operations (filter/map/reduce) — see built-in functional interfaces in action
- Default Methods — how interfaces can gain helper methods without breaking the single-abstract-method rule
- Comparator — a classic functional interface for sorting, now turbo-charged with lambdas