A functional interface is an interface with exactly one abstract method, and it is the type a lambda implements. Java ships a set of them in java.util.function so you rarely have to write your own. A method reference is the other half of the story: a compact way to point at an existing method where a lambda would call it.
What makes an interface functional?
Exactly one abstract method. Everything else can be default or static methods, and any number of private helpers. The @FunctionalInterface annotation is not required, but it is worth adding: the compiler then checks the rule for you and fails the build if a second abstract method ever appears.
@FunctionalInterface
interface Validator {
boolean isValid(String value);
default String describe() {
return "validator: " + this.getClass().getSimpleName();
}
}
public class CustomFunctionalInterface {
public static void main(String[] args) {
Validator notEmpty = value -> value != null && !value.isBlank();
Validator numeric = value -> value.chars().allMatch(Character::isDigit);
System.out.println("notEmpty : " + notEmpty.isValid("text"));
System.out.println("numeric : " + numeric.isValid("12345"));
System.out.println(notEmpty.describe());
}
}notEmpty : true
numeric : true
validator: CustomFunctionalInterface$$Lambda/0x0000025447000c08The describe method is a default method, so it does not count towards the single abstract method. That is what lets a functional interface carry a little shared behaviour - and it is why the standard library's interfaces can have convenience methods like andThen without breaking the rule.
Which built-in interfaces should you know?
Most code you write will use a handful from java.util.function. Each one fixes the shape of the parameters and the return.
| Interface | Shape | Typical use |
|---|---|---|
Function<T, R> | one input, one result | Transforming one value into another |
BiFunction<T, U, R> | two inputs, one result | Combining two values |
Supplier<T> | no input, one result | Creating a value on demand |
Consumer<T> | one input, no result | Doing something with a value |
BiConsumer<T, U> | two inputs, no result | Logging or storing two values |
Predicate<T> | one input, a boolean | Testing a value |
BiPredicate<T, U> | two inputs, a boolean | Comparing two values |
UnaryOperator<T> | one input, same type out | Transforming in place of the type |
BinaryOperator<T> | two inputs, same type out | Combining two values of a type |
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class BuiltInInterfaces {
public static void main(String[] args) {
Function<String, Integer> length = text -> text.length();
Supplier<String> greeting = () -> "hello";
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
java.util.function.Consumer<String> printer = text -> System.out.println("got " + text);
java.util.function.Predicate<String> isLong = text -> text.length() > 3;
System.out.println("length : " + length.apply("tutorsbot"));
System.out.println("greeting : " + greeting.get());
System.out.println("add : " + add.apply(2, 3));
printer.accept("value");
System.out.println("isLong : " + isLong.test("tutorsbot"));
System.out.println("andThen : " + length.andThen(v -> v * 2).apply("abc"));
}
}length : 9
greeting : hello
add : 5
got value
isLong : true
andThen : 6The andThen line shows a feature worth knowing about: most of these interfaces can be chained. length.andThen(v -> v * 2) first measures a string, then doubles the number, giving a function that returns a different type from the one it started with.
What are the four forms of method reference?
A method reference always starts with two colons, and the part before them says where the method lives.
- Static:
TypeName::staticMethod - Instance on a value you already have:
value::instanceMethod - Instance on the first parameter:
TypeName::instanceMethod - Constructor:
TypeName::new
import java.util.function.Function;
import java.util.function.Supplier;
public class MethodReferences {
static String shout(String text) {
return text.toUpperCase() + "!";
}
static int parse(String text) {
return Integer.parseInt(text);
}
static String prefix() {
return "order-";
}
public static void main(String[] args) {
Function<String, String> staticRef = MethodReferences::shout;
Function<String, Integer> staticWithArg = MethodReferences::parse;
Supplier<String> staticNoArg = MethodReferences::prefix;
Function<String, Integer> instanceRef = Integer::parseInt;
Supplier<Integer> boundRef = "tutorsbot"::length;
System.out.println("static : " + staticRef.apply("hi"));
System.out.println("static with arg : " + staticWithArg.apply("42"));
System.out.println("static no arg : " + staticNoArg.get());
System.out.println("instance : " + instanceRef.apply("7"));
System.out.println("bound : " + boundRef.get());
}
}static : HI!
static with arg : 42
static no arg : order-
instance : 7
bound : 9Read each line as "treat this as a function". staticRef is a function from string to string that calls the static method. instanceRef is a function from string to integer that calls parseInt on the argument - the argument plays the role of the receiver. boundRef is a supplier that calls length on a receiver that is already fixed, the string "tutorsbot", and hands back its length. The bound form is the one that confuses people most, and it is what lets you pass a method that needs a receiver as if it were an ordinary function of its own arguments.
How do constructor references work?
TypeName::new creates an instance every time the function is called. With no arguments it matches a no-arg constructor; with arguments it matches the constructor that takes them.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
public class ConstructorRefs {
record Order(String id, double amount) { }
static class Ticket {
private final String id;
Ticket(String id) {
this.id = id;
}
String id() {
return id;
}
}
public static void main(String[] args) {
Supplier<ArrayList<String>> listFactory = ArrayList::new;
Function<String, Ticket> ticketFactory = Ticket::new;
Function<String, Order> partial = id -> new Order(id, 0.0);
List<String> names = listFactory.get();
names.add("ana");
System.out.println("list : " + names);
System.out.println("ticket : " + ticketFactory.apply("T-9").id());
System.out.println("partial : " + partial.apply("A-2"));
}
}list : [ana]
ticket : T-9
partial : Order[id=A-2, amount=0.0]The partial line is a plain lambda rather than a method reference, because Order has no single-argument constructor - the record needs both fields. A method reference cannot invent a default value, so a lambda is the right tool when part of the construction is hard-coded.
When should you use which?
Use a lambda when the behaviour is short and specific to that call site. Use a method reference when the lambda does nothing but call an existing method - it is shorter, and it names the intent better. Use a named method when the logic is more than a line, and extract it rather than writing a long lambda.
list.forEach(System.out::println)- clearly better thanlist.forEach(x -> System.out.println(x)).stream.map(String::trim)- better thanstream.map(s -> s.trim()).stream.map(s -> s.length() * 2)- no matching method exists, so the lambda stays.
What else can a functional interface contain?
A functional interface may have any number of default methods, static methods and private helper methods. Only the single abstract method matters for the lambda contract; the rest are ordinary members that every implementor gets for free.
public class RichInterface {
interface Stringifier {
String convert(String value);
default String withPrefix(String prefix) {
return prefix + convert("value");
}
static Stringifier plain() {
return value -> value;
}
}
public static void main(String[] args) {
Stringifier upper = value -> value.toUpperCase();
System.out.println("convert : " + upper.convert("ana"));
System.out.println("default method : " + upper.withPrefix("> "));
System.out.println("static factory : " + Stringifier.plain().convert("bo"));
}
}convert : ANA
default method : > VALUE
static factory : boThe interface above extends Function, so it inherits apply, and it adds one abstract method plus a default method that builds on both. The default method is a genuine convenience: implementors get it without writing anything, and it can call the abstract method and the inherited one together.
The same trick works for composing functions. andThen and compose on Function let you build a pipeline of transformations in one expression, and a custom interface can add its own composition method with the same shape.
The habits to keep
- Annotate your own functional interfaces with
@FunctionalInterface. - Prefer the built-in interfaces in
java.util.functionbefore writing your own. - Use a method reference when the lambda only forwards to a method.
- Remember the bound form -
value::method- turns a two-argument method into a one-argument function. - Do not force an interface to be functional with default methods if a built-in one fits.
Try it yourself: write a small utility class with one static method, one instance method and one constructor. Then use all four reference forms against them: a static method as a Function, an instance method as a Function whose first argument is the receiver, a bound instance method, and the constructor as a Supplier. Print each result so you can see which reference produced which behaviour.