You need the customer emails from a list of orders. The helper that will do the work does not care about Order. It cares about “given one element, give me another.” Before Java 8 that meant a one-method Mapper you would never reuse, or an anonymous class whose apply was a single return.
Function<T,R> is that mapper, already in the JDK.
A Function turns one value into another. The series hub owns the glossary; here we only care about apply, the two composition defaults, identity, and the slots that already take a Function.
The mapper you used to write by hand
A typical pre-lambda helper looked like this:
interface Mapper<T, R> {
R map(T value);
}
List<String> emails = new ArrayList<>();
for (Order order : orders) {
emails.add(new Mapper<Order, String>() {
@Override
public String map(Order order) {
return order.customerEmail();
}
}.map(order));
}
Nobody enjoyed that. The same intent with a Function is one method reference and a JDK slot:
Function<Order, String> emailOf = Order::customerEmail;
List<String> emails = orders.stream()
.map(emailOf)
.toList();
Order::customerEmail already is the mapper. Skip the named variable when the slot is obvious:
List<String> emails = orders.stream()
.map(Order::customerEmail)
.toList();
Same result. The type is Function<Order, String> whether you spell it or pass the method reference into map. Streams is where this slot shows up every day; this post is the type that fills it.
The contract
Here is the type, trimmed to the methods this post uses:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { … }
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { … }
static <T> Function<T, T> identity() { … }
}
apply is the SAM. andThen and compose chain mappings; identity is the no-op.
| Method | Job |
|---|---|
apply(t) | Run the mapping |
andThen(after) | This function first, then after |
compose(before) | before first, then this function |
identity() | Return the argument unchanged |
andThen and compose are the same idea in opposite reading order. f.andThen(g) is “f, then g.” f.compose(g) is “g, then f.” Prefer andThen when you are chaining left to right the way a Stream already reads.
Function<Order, String> emailOf = Order::customerEmail;
Function<String, String> domainOf = email -> email.substring(email.indexOf('@') + 1);
Function<Order, String> domainFromOrder = emailOf.andThen(domainOf);
String domain = domainFromOrder.apply(order);
identity is the “do nothing” map. You reach for it when an API requires a Function and you genuinely have nothing to transform — Collectors.toMap(Order::id, Function.identity()) keeps the Order as the value.
Where the JDK already asks for Function
You do not invent a mapper type for these slots. You fill them. Order::id and LineItem::sku are the same fill: one value in, another out.
| API | Slot | Typical lambda |
|---|---|---|
Stream.map | Function<? super T, ? extends R> | Order::id |
Stream.flatMap | Function<? super T, ? extends Stream<? extends R>> | o -> o.items().stream() |
Optional.map | Function<? super T, ? extends U> | Order::customerEmail |
Optional.flatMap | Function<? super T, ? extends Optional<? extends U>> | this::findOrder |
Collectors.toMap | key and value mappers | Order::id, Function.identity() |
Collectors.groupingBy | classifier | Order::customerEmail |
Map.compute / computeIfPresent | remapping Function (or BiFunction) | see Bi-arity |
Comparator.comparing | Function<? super T, ? extends U> | Order::total |
A Stream pipeline is the most common place you will see all four shapes together:
List<String> domains = orders.stream()
.filter(Order::active) // Predicate
.map(Order::customerEmail) // Function
.map(email -> email.substring(email.indexOf('@') + 1))
.distinct()
.toList();
filter is the next post. map is this one.
Lambdas, method references, constructors
All three fill a Function slot:
Function<Order, String> lambda = order -> order.id();
Function<Order, String> methodRef = Order::id;
Function<String, Order> ctor = id -> new Order(id, "", List.of(), BigDecimal.ZERO, true);
Instance method references on the argument (Order::id) match Function<Order, String>. A bound instance (order::id) is a Supplier<String> — no argument left to apply. Constructor references work the same way when the constructor’s parameters match the slot. The slot decides which writing is legal.
Named domain type vs Function
Function<Order, BigDecimal> compiles as a discount. Strategy still names DiscountPolicy because the rest of checkout already says payable, tests fake that type, and the next promotion is a class:
@FunctionalInterface
public interface DiscountPolicy {
BigDecimal payable(Order order);
}
DiscountPolicy tenPercent = order -> order.total().multiply(new BigDecimal("0.90"));
DiscountPolicy is a Function in SAM shape. It is not a Function in the type system, and that is the point: the verb belongs to your domain. Use Function when the JDK (or a helper you wrote to be generic) already declared it. Use a named SAM when other people will implement the verb.
Pitfalls
Checked exceptions. apply does not declare them. A mapper that calls Files.readString cannot just throw IOException. Wrap at the boundary with a policy, or preprocess the data so the Function stays a Function. Empty catch inside apply is how production maps swallow failures. The policy is Exceptions. Streams Advanced hits the same wall.
Null. Function does not forbid returning null. Optional.map treats a null result as empty; Stream.map will happily put null in the list. Prefer Optional or a filter if absence is the real answer.
Mutable capture. A Function that increments a field on the enclosing class is not a mapping. It is a side effect wearing a Function slot. That work belongs on a Consumer — PaymentGateway.charge is that shape — or outside the pipeline.
Boxing on hot numeric maps. Function<LineItem, Integer> boxes every quantity(). If a profiler is pointing at that allocation, primitive specializations (ToIntFunction, mapToInt) exist for that path — not as a default for ordinary application code. Keep Order.total() as BigDecimal.
Cheat sheet
One screen for the contract, the usual slots, and the habits that matter:
SAM R apply(T t)
Defaults andThen (this then after) compose (before then this)
Static identity() argument in, same argument out
JDK slots Stream.map / flatMap Optional.map / flatMap
Collectors.toMap / groupingBy Comparator.comparing
Do Order::id in a map slot name DiscountPolicy when the verb is yours
Don't MapperImpl around Function empty-catch inside apply
Function as a side effect that is a Consumer
Do:
- Fill
map/toMap/comparingwithFunction(or a method reference that matches it). - Chain with
andThenwhen you are composing two mappings you already named. - Name a domain SAM when Strategy would — a verb, a second implementer, a test double.
Don’t:
- Invent
OrderIdFunctionfor a slot that already takesFunction. - Swallow checked exceptions inside
apply. - Use
Functionto hide a side effect; that is a Consumer.
Wrap-up
Function<T,R> is the JDK name for “turn this into that.” apply is the SAM; andThen / compose chain mappings; identity fills a slot that requires a Function when you have nothing to change. You meet it first in Stream.map. The next shape is the yes/no question filter asks.