JAVA / STREAMS, LAMBDAS AND OPTIONAL
Functional interfaces and the single-method contract
Decide whether any interface can serve as a lambda target, and design your own single-method contracts with default and static helpers.
What you will learn
- Count abstract methods: default, static and Object methods never count.
- Match a lambda body against the descriptor: parameters, return type and throws.
- Add @FunctionalInterface so a stray second abstract method fails at compile time.
- Write your own interface for checked exceptions or a domain-specific method name.
Understanding Functional interfaces and the single-method contract
A lambda expression carries no type of its own: nothing in s -> !s.isEmpty() says which interface it implements or what the method is called. The compiler takes that from the target type, which must be a functional interface, meaning an interface with exactly one abstract method. That method's signature, the function descriptor, fixes the parameter types, the return type and the checked exceptions that are allowed; the lambda supplies only the body and the interface supplies the name. One abstract method is the entire reason for the rule, because with two there would be no way to tell which one the body was meant to implement.
What gets counted is abstract methods, and three kinds of declaration do not add to the count. Default and static methods already have bodies, so there is nothing for a lambda to fill in, and static methods are not even inherited by implementors. Public methods of Object are excluded as well, because every implementing class already inherits equals, hashCode and toString, so redeclaring one asks for no new behaviour that a lambda could provide. That exclusion is why java.util.Comparator, which declares compare, redeclares equals(Object) and carries a pile of default and static methods, is still a perfectly ordinary lambda target.
Matching shapes does not make two functional interfaces interchangeable, because Java's function types are nominal rather than structural: a value typed as your own Rule cannot be passed where a Check is expected even when both declare boolean test(String), and you have to bridge them explicitly. For the same reason a lambda cannot be assigned to Object or to var, since neither offers a descriptor to check the body against, and abstract classes are never lambda targets however few methods they have. The @FunctionalInterface annotation creates none of this behaviour; it only makes the compiler verify the single-method rule at the interface declaration, so an accidental second abstract method breaks there instead of at every call site.
Reuse before you invent: Predicate, Function, Supplier, Consumer, their Bi and primitive variants, plus Runnable, Callable and Comparator, already cover most descriptors. Writing your own pays off when the descriptor needs a checked exception, an unusual arity, or a method name that documents the domain.
public class SingleMethodContract {
@FunctionalInterface
interface Rule {
boolean test(String input); // the one abstract method
default Rule negate() { // has a body, not counted
return s -> !this.test(s);
}
static Rule always(boolean answer) { // not inherited, not counted
return s -> answer;
}
@Override
boolean equals(Object other); // public Object method, not counted
}
@FunctionalInterface
interface Check { // identical descriptor, unrelated type
boolean test(String input);
}
static int count(String[] words, Rule rule) {
int n = 0;
for (String w : words) {
if (rule.test(w)) {
n++;
}
}
return n;
}
public static void main(String[] args) {
String[] words = { "java", "", "lambda", "", "sam" };
Rule nonEmpty = s -> !s.isEmpty();
System.out.println("non-empty : " + count(words, nonEmpty));
System.out.println("empty : " + count(words, nonEmpty.negate()));
System.out.println("always : " + count(words, Rule.always(true)));
System.out.println("is a Check: " + (nonEmpty instanceof Check));
Check bridged = nonEmpty::test;
System.out.println("bridged : " + bridged.test("sam"));
}
}A functional interface is an interface with exactly one abstract method, and that method's signature is the only thing a lambda body is checked against.
Worked examples
A descriptor that permits a checked exception
Shows that the throws clause is part of the contract, which is why IO code needs its own interface instead of Supplier.
import java.io.IOException;
public class ThrowingContract {
@FunctionalInterface
interface IoTask<T> {
T run() throws IOException;
}
static <T> T attempt(IoTask<T> task, T fallback) {
try {
return task.run();
} catch (IOException e) {
System.out.println("caught: " + e.getMessage());
return fallback;
}
}
public static void main(String[] args) {
System.out.println(attempt(() -> "config.json", "none"));
System.out.println(attempt(() -> { throw new IOException("no such file"); }, "none"));
}
}Example explained
Line 1T run() throws IOException puts the checked exception in the descriptor, so a lambda body is allowed to throw it.
Line 2The same body against Supplier<T> would not compile, because Supplier.get declares no throws and a lambda may not throw more than its descriptor permits.
Line 3The block body that always throws is still value-compatible, since it never completes normally and so never needs a return.
Line 4attempt handles IOException once at the point where the descriptor is invoked, which keeps every caller a plain lambda.
One body, three target types
Demonstrates that the target type alone decides which interface a lambda becomes, and that identical shapes are still unrelated types.
import java.util.function.Function;
import java.util.function.ToIntFunction;
public class TargetTyping {
@FunctionalInterface
interface Sizer {
int measure(String text);
}
public static void main(String[] args) {
Function<String, Integer> boxed = s -> s.length();
ToIntFunction<String> primitive = s -> s.length();
Sizer custom = s -> s.length();
System.out.println(boxed.apply("descriptor"));
System.out.println(primitive.applyAsInt("descriptor"));
System.out.println(custom.measure("descriptor"));
Object asObject = custom;
System.out.println(asObject instanceof Function);
}
}Example explained
Line 1Three identical bodies get three unrelated types; the declared type on the left is the only thing that chose them.
Line 2Each interface names its method differently (apply, applyAsInt, measure) and the lambda never mentions any of those names.
Line 3ToIntFunction declares int as the return type, so the same body satisfies it without producing an Integer.
Line 4The Sizer value is not a Function even though the shapes agree, because functional types are matched by name, not by structure.
Important notes
@FunctionalInterface has no runtime effect and is not required; an un-annotated single-method interface is still a valid lambda target, but nothing then stops a later commit from adding a method and breaking every call site.
A generic interface such as Function<T,R> is fine, but an abstract method with its own type parameters, like <T> T pick(T a, T b), cannot be written as a lambda at all; use a method reference or an anonymous class.
Common mistakes
Adding a helper method to the interface and forgetting the default keyword: it becomes a second abstract method, the interface stops being functional, and every lambda that targeted it fails with 'multiple non-overriding abstract methods found'.
Wrapping code that throws IOException in a Supplier or Function: the descriptor has no throws clause, so the body must catch the exception itself, which usually ends up as an empty catch block that silently hides failures.
Assuming a one-method abstract class works like a functional interface: lambda conversion is defined only for interfaces, so the compiler rejects it and you are forced back to an anonymous class.
Try it yourself
Change, predict, then run
Declare @FunctionalInterface interface Validator { boolean check(String s); } with a default method and(Validator other) that requires both to pass, then combine a non-empty lambda with a length-under-five lambda and print the result for "", "kiro" and "functional". Now add a second abstract method to Validator and read the exact compile error you get.
Open the Java workspaceCheck your understanding
An interface declares boolean test(String s), redeclares boolean equals(Object o), and adds one default and one static method. What does the functional-interface rule conclude?
- Two abstract methods, so the interface is not functional and @FunctionalInterface is rejected.
- Zero, because the default method already supplies an implementation for the abstract one.
- One, so a lambda can target it: the redeclared equals, the default method and the static method are all outside the count.
- One, but only after removing the redeclared equals, since an interface may not declare methods of Object.
Show answer
Public methods of Object are excluded from the count because every implementing class already inherits an implementation, and default and static methods have bodies, so only test(String) is left; this is exactly the shape of java.util.Comparator. The first option is tempting because equals(Object) really is declared abstract there, but the rule counts abstract methods excluding public Object methods, so that declaration can never change the total.