Writing Your Own Functional Interface
Declare a named one-method interface when the built-in types do not fit, such as handlers, middleware and functions that throw checked exceptions.
@FunctionalInterface
interface Name<T, R> {
R method(T arg) throws SomeException; // exactly one abstract method
default Name<T, R> helper() { ... } // optional
}Any interface with exactly one abstract method is a functional interface, so lambdas and method references can implement it. The java.util.function types cover the common shapes, and you should use them when they fit, because they come with andThen, and and friends and every library already accepts them. Write your own when they do not fit.
When your own interface is the better choice
- Checked exceptions. None of the
java.util.functioninterfaces let their method throw a checked exception (Callable, whosecall()declaresthrows Exception, only covers the no-argument case). ACheckedFunctionwhose method declaresthrows Exceptionlets you write lambdas around JDBC or file code and decide in one place how to wrap failures. - A name that means something.
HandlerwithResponse handle(Request req)documents itself.Function<Request, Response>works, but the name and the parameter name are lost, and the type can be passed anywhere any other function of that shape is expected. - More than two parameters, or a mix of primitives. There is no built-in
TriFunction, and a pricing rule taking(long pence, int quantity, String country)would box thelongand theinton every call if forced into generic types. - Domain helpers. Default and static methods on your interface can carry composition that suits your domain, such as chaining middleware.
The rules
@FunctionalInterface
interface Handler {
Response handle(Request req); // the one abstract method
default Handler withHeader(String name) { ... } // default methods are fine
static Handler notFound() { ... } // so are static methods
}
@FunctionalInterface is optional, but add it. The compiler then reports an error on the interface itself if someone later adds a second abstract method, instead of the failure surfacing at every lambda that implements it, possibly in other modules. Abstract methods that match public methods of Object, such as boolean equals(Object o), do not count towards the one.
A lambda cannot implement an interface whose single method is itself generic, such as <T> T convert(String s), because a lambda has nowhere to declare the type parameter. Put the type parameter on the interface instead: interface Converter<T> { T convert(String s); }.
The back-end angle
Web frameworks are built from interfaces like these. A handler turns a request into a response; a middleware or filter takes a handler and returns a new handler that does something before or after it, such as checking authorisation or logging timing. Seeing both as one-method interfaces makes the pattern clear: the example below builds a small pipeline out of nothing but lambdas.
Example
When to use it
- Model request handlers and middleware for a lightweight HTTP service as one-method interfaces composed with lambdas.
- Wrap JDBC calls that throw SQLException in a CheckedFunction so a helper can translate failures in one place.
- Define a PriceRule or DiscountRule interface so pricing strategies can be registered by name in a map.
More examples
A function that may throw a checked exception
The lambda passed to unchecked may throw the checked ConfigException because CheckedFunction.apply declares throws Exception. The adapter wraps it once, keeping the original as the cause and restoring the interrupt flag if the failure was an InterruptedException.
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Exception;
// Adapts it to a plain Function for APIs that only accept java.util.function types.
static <T, R> Function<T, R> unchecked(CheckedFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt(); // keep the interrupt visible to the caller
}
throw new IllegalStateException(e.getMessage(), e);
}
};
}
}
class ConfigException extends Exception {
ConfigException(String message) {
super(message);
}
}
int parsePort(String value) throws ConfigException {
int port = Integer.parseInt(value);
if (port < 1 || port > 65535) {
throw new ConfigException("port out of range: " + port);
}
return port;
}
void main() {
Function<String, Integer> port = CheckedFunction.unchecked(s -> parsePort(s));
for (String value : List.of("8080", "70000")) {
try {
IO.println("port " + port.apply(value));
} catch (IllegalStateException e) {
IO.println("bad config: " + e.getMessage());
}
}
}
// Output:
// port 8080
// bad config: port out of range: 70000Three parameters without boxing
No built-in interface takes three arguments, and a generic one would box every long and int. A named interface keeps the parameter meaning visible at every call site.
@FunctionalInterface
interface PriceRule {
long apply(long pence, int quantity, String country);
}
void main() {
Map<String, PriceRule> rules = new LinkedHashMap<>();
rules.put("standard", (pence, qty, country) -> pence * qty);
rules.put("bulk", (pence, qty, country) -> qty >= 10 ? pence * qty * 90 / 100 : pence * qty);
rules.put("uk-vat", (pence, qty, country) ->
country.equals("GB") ? pence * qty * 120 / 100 : pence * qty);
rules.forEach((name, rule) -> IO.println(name + " " + rule.apply(1250, 10, "GB")));
}
// Output:
// standard 12500
// bulk 11250
// uk-vat 15000Default methods and Object methods do not count
The annotation accepts this interface because equals is inherited from Object and the other methods have bodies. The default and static methods give callers a small composition API of their own.
@FunctionalInterface
interface Validator {
Optional<String> validate(String value); // the single abstract method
boolean equals(Object other); // matches Object, so it does not count
default Validator and(Validator next) {
return value -> {
Optional<String> error = validate(value);
return error.isPresent() ? error : next.validate(value);
};
}
static Validator maxLength(int max) {
return value -> value.length() > max
? Optional.of("longer than " + max)
: Optional.empty();
}
}
void main() {
Validator notBlank = value -> value.isBlank() ? Optional.of("blank") : Optional.empty();
Validator username = notBlank.and(Validator.maxLength(8));
for (String input : List.of("ada", " ", "averyveryverylongname")) {
IO.println("'" + input + "' " + username.validate(input).orElse("ok"));
}
}
// Output:
// 'ada' ok
// ' ' blank
// 'averyveryverylongname' longer than 8
Discussion