Functional Interfaces
A functional interface has exactly one abstract method, which is what makes it usable as the target of a lambda expression — and java.util.function ships a whole toolkit of ready-made ones.
A functional interface is an interface with exactly one abstract method (a 'Single Abstract Method' or SAM interface). This single-method shape is what allows a lambda expression or method reference to serve as an implementation. Java's java.util.function package provides a standard library of general-purpose functional interfaces covering the most common shapes of behavior.
What Makes an Interface Functional
An interface qualifies as functional if it declares exactly one abstract method. It may also declare any number of default and static methods (which have bodies and don't count toward the one-abstract-method rule), plus any methods inherited from Object (like equals, hashCode, toString) don't count either, since a lambda implicitly gets those from whatever object backs it.
interface Greeter {
String greet(String name); // the single abstract method
default String greetLoudly(String name) { // does NOT count against the SAM rule
return greet(name).toUpperCase() + "!";
}
}
Greeter g = name -> "Hello, " + name;
System.out.println(g.greet("Ann")); // Hello, Ann
System.out.println(g.greetLoudly("Ann")); // HELLO, ANN!@FunctionalInterface Annotation
@FunctionalInterface is optional but strongly recommended: it doesn't change runtime behavior, but the compiler enforces the single-abstract-method rule when it's present, causing a compile error if someone later adds a second abstract method and breaks the contract.
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input);
// adding a second abstract method here would now be a COMPILE ERROR
}Predicate<T>
Predicate<T> represents a boolean-valued test: boolean test(T t). Commonly used for filtering.
import java.util.function.Predicate;
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive); // combinator
System.out.println(isEvenAndPositive.test(-4)); // false
System.out.println(isEven.negate().test(4)); // falseFunction<T, R>
Function<T, R> represents a transformation: R apply(T t). It supports composition via andThen (apply this, then the argument) and compose (apply the argument, then this).
import java.util.function.Function;
Function<Integer, Integer> doubleIt = x -> x * 2;
Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> doubleThenAddOne = doubleIt.andThen(addOne);
System.out.println(doubleThenAddOne.apply(5)); // (5*2)+1 = 11
Function<Integer, Integer> addOneThenDouble = doubleIt.compose(addOne);
System.out.println(addOneThenDouble.apply(5)); // (5+1)*2 = 12Consumer<T>
Consumer<T> represents an operation that takes an argument and returns nothing: void accept(T t). Used for side effects like printing, logging, or collecting.
import java.util.function.Consumer;
Consumer<String> printer = s -> System.out.println("Got: " + s);
List.of("a", "b").forEach(printer); // forEach takes a Consumer
Consumer<String> logger = s -> System.out.println("Logged: " + s);
Consumer<String> combined = printer.andThen(logger); // runs both in sequence
combined.accept("x");Supplier<T>
Supplier<T> represents a source of values that takes no arguments: T get(). Used for lazy or deferred value production.
import java.util.function.Supplier;
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get());
// Lazy default value example:
Supplier<String> defaultName = () -> computeExpensiveDefault();
String name = (userInput != null) ? userInput : defaultName.get(); // only computed if neededUnaryOperator<T> and BinaryOperator<T>
UnaryOperator<T> extends Function<T, T> for the special case where the input and output types are the same. BinaryOperator<T> extends BiFunction<T, T, T> for combining two values of the same type into one — this is exactly the shape Collections.reduce-style operations and Comparator-adjacent utilities need.
import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;
UnaryOperator<Integer> increment = n -> n + 1;
System.out.println(increment.apply(5)); // 6
BinaryOperator<Integer> max = BinaryOperator.maxBy(Integer::compareTo);
System.out.println(max.apply(3, 8)); // 8Other Common Functional Interfaces
BiFunction<T, U, R> (two-argument function), BiConsumer<T, U> (two-argument consumer), BiPredicate<T, U> (two-argument predicate), and primitive specializations (IntPredicate, ToIntFunction<T>, IntSupplier, etc.) avoid autoboxing overhead for common primitive cases.
import java.util.function.*;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(2, 3)); // 5
IntPredicate isOddPrimitive = n -> n % 2 != 0; // no boxing to Integer needed
System.out.println(isOddPrimitive.test(7)); // trueCustom Functional Interfaces
When none of the standard shapes fit — e.g. three arguments, a checked exception, or a domain-specific name that improves readability — define your own functional interface.
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
}
TriFunction<Integer, Integer, Integer, Integer> sum3 = (a, b, c) -> a + b + c;
System.out.println(sum3.apply(1, 2, 3)); // 6Practical Usage
Functional interfaces are the backbone of the Streams API (filter takes a Predicate, map takes a Function, forEach takes a Consumer) and are widely used for callbacks, strategy objects, and validation logic passed as parameters.
List<String> names = List.of("Ann", "bob", "Carl", "dan");
long count = names.stream()
.filter(s -> Character.isUpperCase(s.charAt(0))) // Predicate<String>
.count();
System.out.println(count); // 2Edge Cases
A lambda targeting a functional interface whose method throws a checked exception must match; if the standard java.util.function interfaces (which declare no checked exceptions) don't fit code that must throw a checked exception, you either wrap it in a try/catch inside the lambda, rethrow as unchecked, or define a custom functional interface that declares throws.
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Exception; // custom interface allowed to declare checked exceptions
}Complete Example
import java.util.*;
import java.util.function.*;
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
List<Integer> nums = List.of(-3, 4, -1, 9, 0, 6);
Predicate<Integer> isPositive = n -> n > 0;
Function<Integer, Integer> square = n -> n * n;
Consumer<Integer> print = n -> System.out.println("Result: " + n);
for (int n : nums) {
if (isPositive.test(n)) {
print.accept(square.apply(n));
}
}
}
}Mental Model
Think of java.util.function as a small set of labeled 'shapes of behavior' — takes-one-returns-boolean (Predicate), takes-one-returns-something (Function), takes-one-returns-nothing (Consumer), takes-nothing-returns-something (Supplier) — and a lambda is just a value poured into whichever shape the API expects.
Quick Reference
Predicate<T> boolean test(T t)
Function<T,R> R apply(T t)
Consumer<T> void accept(T t)
Supplier<T> T get()
UnaryOperator<T> T apply(T t) // Function<T,T>
BinaryOperator<T> T apply(T t1, T t2) // BiFunction<T,T,T>
BiFunction<T,U,R> R apply(T t, U u)
BiConsumer<T,U> void accept(T t, U u)
BiPredicate<T,U> boolean test(T t, U u)Key Takeaways
A functional interface has exactly one abstract method; default/static methods and Object methods don't count.
@FunctionalInterface is optional but makes the compiler enforce the single-method contract.
java.util.function provides Predicate, Function, Consumer, Supplier, UnaryOperator, BinaryOperator, and their Bi- variants for the common shapes of behavior.
Predicate/Function support combinators (and/or/negate, andThen/compose) for building complex behavior from simple pieces.
Define a custom functional interface when the standard shapes don't fit, e.g. three-argument functions or checked-exception-throwing operations.
interface Broken { void a(); void b(); // two abstract methods - NOT a functional interface } // Broken x = () -> {}; // does not compile
@FunctionalInterface interface Fixed { void a(); default void b() { /* has a body, doesn't count */ } }
A functional interface must have exactly one abstract method; extra abstract methods disqualify it from being a lambda target unless given default bodies.
Function<Integer, Integer> risky = n -> { if (n < 0) throw new java.io.IOException("bad"); // checked exception, won't compile return n; };
Function<Integer, Integer> safe = n -> { if (n < 0) throw new IllegalArgumentException("bad"); // unchecked, compiles fine return n; };
Function.apply() doesn't declare any checked exceptions, so a lambda implementing it can't throw one either; use an unchecked exception or a custom functional interface that declares throws.
Predicate<String> notEmpty = s -> !s.isEmpty(); Predicate<String> notNull = s -> s != null; Predicate<String> combined = notEmpty.and(notNull); // wrong order - notEmpty runs first and NPEs on null
Predicate<String> combined = notNull.and(notEmpty); // check null first and() evaluates left-to-right with short-circuiting like &&; if the null check isn't first, testing notEmpty on a null string throws NullPointerException before notNull ever runs.
What is the defining requirement for an interface to be a functional interface?
It must declare exactly one abstract method; it may have any number of default methods, static methods, and methods it inherits implicitly from Object, none of which count toward that one-method limit.
What does the @FunctionalInterface annotation actually do at runtime?
Nothing at runtime — it's purely a compile-time check that causes a compile error if the interface doesn't have exactly one abstract method, protecting against someone accidentally adding a second abstract method later.
What is the difference between Function<T,R>.andThen() and .compose()?
andThen(after) applies the original function first, then feeds its result into 'after'. compose(before) applies 'before' first, then feeds its result into the original function — they run in opposite order.
Why can't a lambda targeting java.util.function.Function throw a checked exception?
Function's abstract method apply(T) doesn't declare any checked exceptions in its signature, and an implementing lambda can't throw a checked exception the interface method doesn't declare — this is the same rule that applies to any interface method implementation.
What is the relationship between UnaryOperator<T> and Function<T,R>?
UnaryOperator<T> extends Function<T,T> — it's the special case where the input and output types are identical, so it doesn't need a separate R type parameter.
When would you define a custom functional interface instead of using one from java.util.function?
When the standard shapes don't fit: you need more than two parameters, need to declare a checked exception, or want a domain-specific method/interface name that makes calling code more self-documenting than a generic apply/test/accept.
Why does Predicate.and()'s evaluation order matter when one predicate might throw for certain inputs?
and() short-circuits left-to-right like &&, evaluating the first predicate before the second; if the first predicate would throw (e.g. calling a method on a value the second predicate was meant to null-check), ordering the null-safe check first avoids the exception.
Write a program that combines two Predicate<Integer> instances (isPositive and isEven) using and(), or(), and negate(), printing the result for the value -4.
import java.util.function.Predicate; public class PredicateCombinators { public static void main(String[] args) { Predicate<Integer> isPositive = n -> n > 0; Predicate<Integer> isEven = n -> n % 2 == 0; int value = -4; System.out.println("and: " + isPositive.and(isEven).test(value)); // false (not positive) System.out.println("or: " + isPositive.or(isEven).test(value)); // true (is even) System.out.println("negate: " + isPositive.negate().test(value)); // true (not positive) } } and: false (fails isPositive first). or: true (passes isEven). negate: true (isPositive.negate() means 'is not positive', and -4 is not positive).
What does this print?
25. length.andThen(square) first computes length("hello") = 5, then feeds it into square: 5*5 = 25.
Write a custom functional interface TriConsumer<A,B,C> with a single method accept(A a, B b, C c), and use it with a lambda to print three values.
public class TriConsumerDemo { @FunctionalInterface interface TriConsumer<A, B, C> { void accept(A a, B b, C c); } public static void main(String[] args) { TriConsumer<String, Integer, Boolean> printer = (name, age, active) -> System.out.println(name + ", " + age + ", active=" + active); printer.accept("Ann", 30, true); } }
The custom TriConsumer interface has exactly one abstract method, so a three-parameter lambda can implement it. Output: Ann, 30, active=true
Function<String, Integer> length = String::length;
Function<Integer, Integer> square = n -> n * n;
Function<String, Integer> combined = length.andThen(square);
System.out.println(combined.apply("hello"));