You need to pass “how to get an order id” into a helper. Before Java 8 that meant a one-method interface, an anonymous class, and twelve lines of ceremony for one return order.id(). The helper did not care about the class. It cared about one method.
That gap — pass a behavior, not a type you will never name again — is what java.util.function is for.
A functional interface is a type with one abstract method. A lambda is an instance of that type, not a new kind of value. The package is a catalog of those one-method shapes so you stop inventing IdExtractor and OrderFilter for jobs the JDK already named.
This post is the series glossary and index: the four shapes, how lambdas attach to them, when a named domain type still wins, and a living catalog of every post in this section. Later posts link back here instead of re-lecturing SAM, target typing, or @FunctionalInterface.
How to use this page
You do not need to read eight posts before you write order -> order.id().
- Jump by shape — Function, Predicate, Consumer, Supplier — if you already know the name and want the contract, the default methods, and where the JDK asks for it.
- Follow Start here if you want the four Stream workhorses first, then the two-arg and primitive specializations.
- Treat this page as the map. Family posts stand alone for their own title. Definitions of SAM, lambdas, method references, constructor references, target typing, and cargo-cult failure modes live here.
What this series unlocks, not what it replaces: Java Streams already uses these types. This series teaches the types.
When they shipped
| Release | Status | Notes |
|---|---|---|
| Java 8 | Standard library | java.util.function and lambda / method-reference syntax |
| Java 11 | Small addition | Predicate.not |
| Today | Core skill | Every modern JDK still uses this model |
Use Java 8+ for everything in this series. Examples assume a current JDK; Predicate.not is called out where it appears.
What a functional interface actually is
One abstract method. That is the whole rule. The compiler calls it a SAM type (single abstract method). Default methods, static methods, and the methods every object already has (equals, hashCode, toString) do not count against the one.
@FunctionalInterface
public interface DiscountPolicy {
BigDecimal payable(Order order);
}
@FunctionalInterface is documentation plus a compiler check. Omit it and a SAM type is still a functional interface. Put it on a type with two abstract methods and the compiler refuses the file. The annotation does not create the capability; it locks the contract so a later edit cannot silently add a second abstract method.
Because there is one method, these three writings are the same instance:
DiscountPolicy percentOff = new DiscountPolicy() {
@Override
public BigDecimal payable(Order order) {
return order.total().multiply(new BigDecimal("0.90"));
}
};
DiscountPolicy lambda = order -> order.total().multiply(new BigDecimal("0.90"));
DiscountPolicy methodRef = this::tenPercentOff;
The anonymous class is what you wrote before Java 8. The lambda is the same SAM instance with the boilerplate stripped. The method reference is a lambda whose body is already a method. Later posts use all three without repeating this paragraph.
Note: A lambda is not a hidden inner class you should reason about as a class. It is a value whose type is whatever SAM the compiler is trying to fill. That filling-in is target typing.
How a lambda gets a type
The compiler infers the SAM from the slot the lambda sits in — a variable type, a method parameter, a return type — not from the lambda body alone.
Function<Order, String> idOf = Order::id; // slot is Function
Predicate<Order> active = Order::active; // slot is Predicate
ids.removeIf(id -> id.startsWith("tmp-")); // slot is Predicate from removeIf
Function<String, BigDecimal> money = BigDecimal::new; // constructor ref; slot picks the ctor
That is why var cannot infer a lambda:
var idOf = Order::id; // does not compile — no target type
There is no unique SAM for Order::id. It could be Function<Order, String>, a home-grown IdExtractor, or anything else with one matching method. Write the type on the left, or pass the lambda into a method that already declared the SAM.
Constructor references follow the same rule: the slot picks the constructor and the SAM. BigDecimal::new fills Function<String, BigDecimal> because that constructor takes one argument. A no-arg constructor can fill a Supplier; two arguments can fill a BiFunction. The lab Order canonical constructor takes five arguments, so it does not match a package type — later posts write id -> new Order(...) instead of Order::new.
The four shapes, one sentence each
Everything in java.util.function is one of these four, then either a second argument, a same-type restriction, or a primitive so you do not box.
Function — T in, R out
A Function turns one value into another.
Stream.map, Optional.map, Collectors.toMap. Details in Function: Turn T into R Without a One-Off Interface.
Predicate — T in, boolean out
A Predicate answers a yes/no question about a value.
Stream.filter, anyMatch, Collection.removeIf. Details in Predicate: Ask a Yes/No Question You Can Combine.
Consumer — T in, no result
A Consumer accepts a value in order to do something — log, persist, notify — not to return a new value.
Stream.forEach, Optional.ifPresent, Iterable.forEach. Details in Consumer: Accept a Value for a Side Effect.
Supplier — no input, T out
A Supplier produces a value when asked, so the caller can wait until it actually needs one.
Optional.orElseGet, CompletableFuture.supplyAsync, factories, lazy init. Details in Supplier: Produce a Value When Asked.
| Shape | SAM | Typical JDK slot |
|---|---|---|
| Function | R apply(T t) | map, compute |
| Predicate | boolean test(T t) | filter, removeIf |
| Consumer | void accept(T t) | forEach, ifPresent |
| Supplier | T get() | orElseGet, supplyAsync |
Same-type Functions are UnaryOperator and BinaryOperator. Two-argument variants are BiFunction, BiPredicate, and BiConsumer. The primitive grid is Primitive Functional Interfaces.
Default methods do not break SAM
Function.andThen, Function.compose, Predicate.and / or / negate, Consumer.andThen are default methods. They are convenience on the instance; they are not a second abstract method. Object methods never counted either. You can still write a lambda for a type that has a rich default-method API.
The lab domain: order, discount, payment
Every post in this series works the same tiny checkout domain as the SOLID and design-patterns series, so you are not re-learning a scenario.
| Type | Role |
|---|---|
Order / LineItem | The data: id, email, items, total, active() |
DiscountPolicy | A named seam — contrast with raw Function<Order, BigDecimal> |
PaymentGateway | A side-effect collaborator (Consumer / BiConsumer) |
Order is a plain data carrier — a good fit for a record:
public record Order(
String id,
String customerEmail,
List<LineItem> items,
BigDecimal total,
boolean active) {}
public record LineItem(String sku, int quantity, BigDecimal unitPrice) {}
If records are new to you, Java Records: Immutable Data Without the Boilerplate covers the shape.
When to write your own vs reuse the package
The package types are for slots the JDK already declared. Stream.map takes a Function. You do not invent OrderIdMapper to please map.
A named domain type wins when the seam has a business name and a second implementation you can point at. DiscountPolicy in the Strategy post is that case: callers say payable, tests fake DiscountPolicy, and the next promotion is a class. Function<Order, BigDecimal> would compile. It would also hide the word the rest of the checkout code already uses.
| Reach for | When |
|---|---|
Function / Predicate / Consumer / Supplier | The JDK (or your helper) already declared that SAM |
A named @FunctionalInterface | The method has a domain verb, and you expect a second implementer or a test double |
| A multi-method interface | The collaborator has a conversation, not one shot — see Interface Segregation |
The JDK type is the default when you are filling a JDK slot. The named type is the default when you are designing a seam other people will implement.
When functional interfaces are cargo-cult
Lambdas go wrong more often from missing a name than from missing a type.
Concrete anti-patterns to refuse:
- A named SAM with one implementer, forever.
OrderIdFunction extends Function<Order, String>plusOrderIdFunctionImpl.Order::idwas enough. Function<Order, String>in a public API that means “order id”. Callers read generics instead of a verb. Give the method a name (idOf(Order)) or give the type a name (DiscountPolicy) if it is a seam.ToIntFunctionin ordinary application code. Primitive specializations exist to skip boxing on hot numeric paths and insideIntStream. They are not a badge. Details in the primitives post.- A lambda that throws a checked exception with a swallowed
catch. None of the types in this package declare checked exceptions. Wrap at the boundary with a policy, or preprocess — do not empty-catch insideapply. The policy is Exceptions. Streams Advanced hits the same wall insidemap. - Capturing a mutable accumulator “because it is a lambda.” A lambda that rewrites a field the rest of the method also touches is a closure over shared state, not a Function. That is the purity rule Streams already teach.
Indirection with no second implementation and no JDK slot is decoration, not design.
Outside this package
These are functional interfaces. They are not in java.util.function, so they do not get their own post in this series:
| Type | Package | SAM |
|---|---|---|
Comparator<T> | java.util | int compare(T a, T b) |
Runnable | java.lang | void run() |
Callable<V> | java.util.concurrent | V call() throws Exception |
You already pass them as lambdas (list.sort((a, b) -> …), new Thread(() -> …)). Callable is the rare SAM that does declare a checked exception — which is why CompletableFuture.supplyAsync takes a Supplier instead, and why people wrap call() at the boundary.
The catalog
Jump to the family that matches the slot you are filling.
Wave 1 — the four Stream workhorses
| Type | Pain it targets | Post |
|---|---|---|
Function<T,R> | Turn each value into another without a one-off mapper type | Function: Turn T into R Without a One-Off Interface |
Predicate<T> | Keep, drop, or match with a yes/no you can compose | Predicate: Ask a Yes/No Question You Can Combine |
Consumer<T> | Run a side effect per value without pretending it is a map | Consumer: Accept a Value for a Side Effect |
Supplier<T> | Wait to create a value until something actually asks | Supplier: Produce a Value When Asked |
Wave 2 — same type, two arguments
| Type | Pain it targets | Post |
|---|---|---|
UnaryOperator<T>, BinaryOperator<T> | Input and output are the same type (replaceAll, reduce) | Unary and Binary Operators: Same Type In, Same Type Out |
BiFunction, BiPredicate, BiConsumer | Two arguments without inventing a pair type (Map.merge) | Bi-Arity: Two Arguments Without a Pair Type |
Wave 3 — skip the box
| Type | Pain it targets | Post |
|---|---|---|
| The remaining ~30 primitive SAMs | Hot numeric paths where Integer is the tax, not the model | Primitive Functional Interfaces: Skip the Box on the Hot Path |
Start here
Package order is a catalog, not a curriculum. If you are picking a first path, start with the types Streams already pass around.
- Function: Turn T into R Without a One-Off Interface
- Predicate: Ask a Yes/No Question You Can Combine
- Consumer: Accept a Value for a Side Effect
- Supplier: Produce a Value When Asked
- Unary and Binary Operators: Same Type In, Same Type Out
- Bi-Arity: Two Arguments Without a Pair Type
- Primitive Functional Interfaces: Skip the Box on the Hot Path
Read the post that matches the slot you have today. Come back here for SAM, target typing, and the “when to skip it” test.
Cheat sheet
SAM one abstract method; defaults and Object methods do not count
Lambda an instance of that SAM; type comes from the slot (target typing)
Method ref Order::id, this::tenPercentOff, BigDecimal::new
Annotation @FunctionalInterface documents and locks; it does not enable lambdas
Four shapes Function T→R | Predicate T→boolean | Consumer T→void | Supplier →T
Then same-type (Unary/BinaryOperator), two-arg (Bi*), primitives (skip boxing)
JDK slot use the package type Stream.map already declared
Domain seam name it (DiscountPolicy) when the verb is yours and a second impl exists
Trigger to stop: one-off XxxFunctionImpl, ToIntFunction in ordinary app code
Not in this map: Comparator, Runnable, Callable (SAM, different packages)
Do:
- Fill JDK slots with the package types; name a SAM when the rest of your code already has a verb for it.
- Write the target type (
Function<Order, String> idOf = Order::id); do not expectvarto invent one. - Wrap checked exceptions at the boundary. None of these interfaces declare them.
Don’t:
- Invent a one-implementer wrapper around
Function“for clarity”. - Treat a lambda as license to mutate whatever the enclosing method can see.
- Reach for a primitive SAM because the grid looks complete. Reach for it when boxing shows up on a hot path, or when
IntStreamalready chose the type.
Wrap-up
java.util.function is four shapes and a grid of specializations, not forty-three unrelated APIs. A functional interface is a SAM. A lambda is an instance of that SAM, typed by the slot it sits in. Use the package types where the JDK already asked; use a named domain SAM where Strategy would — a verb other people implement.
The catalog on this page is the map of the section. Start with Function, because map is the slot you already write.