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

ReleaseStatusNotes
Java 8Standard libraryjava.util.function and lambda / method-reference syntax
Java 11Small additionPredicate.not
TodayCore skillEvery 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.

ShapeSAMTypical JDK slot
FunctionR apply(T t)map, compute
Predicateboolean test(T t)filter, removeIf
Consumervoid accept(T t)forEach, ifPresent
SupplierT 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.

TypeRole
Order / LineItemThe data: id, email, items, total, active()
DiscountPolicyA named seam — contrast with raw Function<Order, BigDecimal>
PaymentGatewayA 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 forWhen
Function / Predicate / Consumer / SupplierThe JDK (or your helper) already declared that SAM
A named @FunctionalInterfaceThe method has a domain verb, and you expect a second implementer or a test double
A multi-method interfaceThe 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> plus OrderIdFunctionImpl. Order::id was 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.
  • ToIntFunction in ordinary application code. Primitive specializations exist to skip boxing on hot numeric paths and inside IntStream. 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 inside apply. The policy is Exceptions. Streams Advanced hits the same wall inside map.
  • 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:

TypePackageSAM
Comparator<T>java.utilint compare(T a, T b)
Runnablejava.langvoid run()
Callable<V>java.util.concurrentV 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

TypePain it targetsPost
Function<T,R>Turn each value into another without a one-off mapper typeFunction: Turn T into R Without a One-Off Interface
Predicate<T>Keep, drop, or match with a yes/no you can composePredicate: Ask a Yes/No Question You Can Combine
Consumer<T>Run a side effect per value without pretending it is a mapConsumer: Accept a Value for a Side Effect
Supplier<T>Wait to create a value until something actually asksSupplier: Produce a Value When Asked

Wave 2 — same type, two arguments

TypePain it targetsPost
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, BiConsumerTwo arguments without inventing a pair type (Map.merge)Bi-Arity: Two Arguments Without a Pair Type

Wave 3 — skip the box

TypePain it targetsPost
The remaining ~30 primitive SAMsHot numeric paths where Integer is the tax, not the modelPrimitive 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.

  1. Function: Turn T into R Without a One-Off Interface
  2. Predicate: Ask a Yes/No Question You Can Combine
  3. Consumer: Accept a Value for a Side Effect
  4. Supplier: Produce a Value When Asked
  5. Unary and Binary Operators: Same Type In, Same Type Out
  6. Bi-Arity: Two Arguments Without a Pair Type
  7. 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 expect var to 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 IntStream already 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.

Continue with Function: Turn T into R Without a One-Off Interface