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.

MethodJob
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.

APISlotTypical lambda
Stream.mapFunction<? super T, ? extends R>Order::id
Stream.flatMapFunction<? super T, ? extends Stream<? extends R>>o -> o.items().stream()
Optional.mapFunction<? super T, ? extends U>Order::customerEmail
Optional.flatMapFunction<? super T, ? extends Optional<? extends U>>this::findOrder
Collectors.toMapkey and value mappersOrder::id, Function.identity()
Collectors.groupingByclassifierOrder::customerEmail
Map.compute / computeIfPresentremapping Function (or BiFunction)see Bi-arity
Comparator.comparingFunction<? 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 / comparing with Function (or a method reference that matches it).
  • Chain with andThen when 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 OrderIdFunction for a slot that already takes Function.
  • Swallow checked exceptions inside apply.
  • Use Function to 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.

Next optional step in the series Keep, drop, or match with a yes/no you can compose. Predicate: Ask a Yes/No Question You Can Combine