Lambda Expressions ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-07

Function Interface in Java

Quick check before the theory: what does f1.andThen(f2).compose(f3).compose(f4).apply("Compose") print if every function appends its own digit to the string? Intuition says Compose1234. The console prints Compose4312. Chaining order is where Function trips people up most often, both in code and in interviews, so we will take that chain apart step by step below.

java.util.function.Function<T, R> is a built-in functional interface from the java.util.function package, added in Java SE 8. It takes one argument of type T and returns a result of type R — the same type or a different one. In other words, it describes a transformation of one value into another.

What the Function interface is

The declaration in the JDK looks like this:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

It has exactly one abstract method, apply(), which is why it carries the @FunctionalInterface annotation and can be implemented with a lambda expression or a method reference.

The functional descriptor of the interface is:

T -> R

Read it as "one value of type T goes in, one value of type R comes out". Typical jobs: String to Integer, entity to DTO, id to object, number to a formatted string.

Is this still current?

Function has not changed since Java 8: the signatures of apply, andThen, compose and identity are identical in Java 8, 11, 17, 21 and 25. Everything in this lesson works on any current LTS release without caveats.

The apply() method

apply() is the single abstract method of the interface. Its body is what you write inside the lambda, and it performs the actual transformation. Here Exam objects are turned into a String:

public class Exam {
    private final String name;
    private final String version;

    public Exam(String name, String version) {
        this.name = name;
        this.version = version;
    }

    public String getName() {
        return name;
    }

    public String getVersion() {
        return version;
    }
}
Function<Exam, String> converter = e -> e.getName() + " " + e.getVersion();

System.out.println(converter.apply(new Exam("Java Core", "8")));
System.out.println(converter.apply(new Exam("Java Core", "17")));
System.out.println(converter.apply(new Exam("Java Core", "21")));

The output is:

Java Core 8
Java Core 17
Java Core 21

A second example is worth a closer look, because three implicit conversions happen in a single line:

import java.util.function.Function;

public class FunctionExample1 {
    public static void main(String[] args) {
        Function<Double, Long> rounder = d -> Math.round(d);
        System.out.println(rounder.apply(5.7)); // 6
    }
}
  • the literal 5.7 (a double) is autoboxed into Double, because apply() accepts an object;
  • inside the lambda that Double is unboxed back to double so that Math.round(double) can be called;
  • the resulting long is boxed into Long, because the second type parameter is declared as Long.

The same lambda can be written as a method reference — the compiler picks the Math.round(double) overload because the target type demands a Long result:

Function<Double, Long> rounder = Math::round;
System.out.println(rounder.apply(5.7)); // 6

andThen and compose: execution order

Besides apply() the interface declares two default methods that glue functions into a chain:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before);

The only difference between them is which function runs first:

Method Runs first Equivalent lambda Reading direction
f.andThen(g) f, then g x -> g.apply(f.apply(x)) left to right, the way you read code
f.compose(g) g, then f x -> f.apply(g.apply(x)) right to left, the way maths writes it

Here is the chain from the opening question, plus a plain andThen chain for contrast:

import java.util.function.Function;

public class FunctionExample2 {
    public static void main(String[] args) {
        Function<String, String> f1 = s -> s + "1";
        Function<String, String> f2 = s -> s + "2";
        Function<String, String> f3 = s -> s + "3";
        Function<String, String> f4 = s -> s + "4";

        System.out.println(f1.andThen(f2).compose(f3).compose(f4).apply("Compose")); // Compose4312
        System.out.println(f1.andThen(f2).andThen(f3).apply("AndThen"));             // AndThen123
    }
}

The first line breaks down as follows. Every compose pushes a function to the front of the chain, so the function attached last is the one executed first:

Step Function Why this one Result
1 f4 the last compose — runs before everything else Compose4
2 f3 the previous compose Compose43
3 f1 the original function the chain started from Compose431
4 f2 andThen — always after f1 Compose4312

The second line is simpler: andThen appends to the end of the chain, so execution order matches the order you wrote — f1, f2, f3 — and the result is AndThen123.

Mnemonic

The two forms always produce the same function: g.compose(f) is equivalent to f.andThen(g). Whenever the order feels unclear, rewrite the chain with andThen only — it reads left to right. Both methods throw NullPointerException if you hand them null: there is an Objects.requireNonNull check inside.

Function.identity()

The interface also declares one static method:

static <T> Function<T, T> identity()

It returns a function that gives back its own argument unchanged — the identity transformation T -> T:

import java.util.function.Function;

public class FunctionExample3 {
    public static void main(String[] args) {
        Function<String, String> f = Function.identity();
        System.out.println(f.apply("Some Value")); // Some Value
    }
}

On its own such a call is useless. identity() earns its keep where an API demands a function but you need the original object as is — for example when the element itself becomes the map key:

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class IdentityExample {
    public static void main(String[] args) {
        List<String> words = List.of("Java", "Function", "Lambda");

        Map<String, Integer> lengthByWord = words.stream()
                .collect(Collectors.toMap(Function.identity(), String::length));

        System.out.println(lengthByWord); // {Java=4, Lambda=6, Function=8}
    }
}

Where Function shows up in the JDK

Function is the most widely used functional interface in the standard library. The entry points you will meet first:

  • Stream.map(Function<? super T, ? extends R>) — transform every element of a stream;
  • Optional.map(Function<? super T, ? extends U>) — transform the value if one is present;
  • Map.computeIfAbsent(K, Function<? super K, ? extends V>) — compute a value for a missing key;
  • Collectors.toMap and Collectors.groupingBy — extract a key from an element;
  • Comparator.comparing(Function<? super T, ? extends U>) — sort by a derived field.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class FunctionInApi {
    public static void main(String[] args) {
        Function<String, Integer> length = String::length;

        // 1. Stream.map accepts a Function
        List<Integer> lengths = List.of("Java", "Function", "Lambda")
                .stream()
                .map(length)
                .toList(); // Stream.toList() is available since Java 16
        System.out.println(lengths); // [4, 8, 6]

        // 2. Map.computeIfAbsent: the Function builds the value for a new key
        Map<Character, List<String>> byFirstLetter = new HashMap<>();
        for (String word : List.of("Java", "Jar", "Lambda")) {
            byFirstLetter.computeIfAbsent(word.charAt(0), k -> new ArrayList<>()).add(word);
        }
        System.out.println(byFirstLetter); // {J=[Java, Jar], L=[Lambda]}
    }
}

BiFunction, UnaryOperator and primitive variants

The plain Function<T, R> works with objects only and takes exactly one argument. For everything else the java.util.function package ships ready-made specializations:

Interface Descriptor Abstract method When to use it
Function<T, R> T -> R apply ordinary object-to-object transformation
BiFunction<T, U, R> (T, U) -> R apply two arguments are needed
UnaryOperator<T> T -> T apply argument and result have the same type
BinaryOperator<T> (T, T) -> T apply reduce two values of one type into one
IntFunction<R>, LongFunction<R>, DoubleFunction<R> int -> R apply primitive in, object out
ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> T -> int applyAsInt and friends object in, primitive out
IntUnaryOperator, IntToDoubleFunction and others int -> int, int -> double applyAsInt, applyAsDouble primitive in, primitive out

Performance note

The primitive specializations are not decoration. Function<Integer, Integer> boxes and unboxes on every single call, while IntUnaryOperator works on int directly. In hot loops and large streams the difference is measurable, so reach for IntUnaryOperator, ToIntFunction and their relatives whenever numbers are involved.

Function, Supplier, Consumer and Predicate

Function is one of the four core functional interfaces in java.util.function. The easiest way to tell them apart is by asking whether they have an input and an output:

Interface Descriptor Method Job
Function<T, R> T -> R apply take a value, return a result
Supplier<T> () -> T get supply a value, no input
Consumer<T> T -> void accept take a value, return nothing
Predicate<T> T -> boolean test check a condition

Technically Predicate<T> could be replaced by Function<T, Boolean>, but you should not: Predicate returns a primitive boolean with no boxing and adds and(), or() and negate().

Where developers get tripped up

  • Mixing up compose and andThen. The single most common mistake, in code and in interviews alike. compose runs the argument function before the current one; andThen runs it after.
  • Expecting Function to modify the original object. A function changes nothing in place — it returns a new value. A bare converter.apply(x); without assigning the result does nothing useful.
  • Trying to throw a checked exception. apply() is declared without throws, so an IOException inside the lambda will not compile; you have to catch it and wrap it in an unchecked exception.
  • Using the object flavour for numbers. Function<Integer, Integer> in a million-iteration loop creates a million boxed values; IntUnaryOperator creates none.
  • Forgetting about null. Function knows nothing about null: if apply() receives null and the lambda calls a method on it, you get a NullPointerException. Guard it yourself or use Optional.map.

The official reference with every signature is in the Javadoc for java.util.function.Function.

Frequently Asked Questions

What is the difference between Function and UnaryOperator?

UnaryOperator extends Function where the argument type and the result type are the same. Any UnaryOperator of String can be passed where a Function from String to String is expected, but not the other way round. Pick UnaryOperator when the transformation does not change the type: the method signature gets shorter and states the intent.

How do you pass two arguments to a Function?

Two options. The standard one is BiFunction, whose apply method takes two parameters. The other is currying, where one function returns another: Function sum = a -> b -> a + b, so sum.apply(2).apply(3) returns 5. Currying helps when the first argument is known early and the second arrives later.

Can a Function throw a checked exception?

No. The apply() method is declared without throws, so a checked exception inside the lambda is a compile error. Your options are to handle it inside the lambda, wrap it in a RuntimeException, or declare your own functional interface whose method is declared with throws.

Why use Function.identity() instead of t -> t?

The behaviour is identical, but Function.identity() returns the same shared instance every time, while the lambda t -> t creates a separate object at each call site. Collectors.toMap(Function.identity(), ...) also reads unambiguously, whereas t -> t buried in a long chain looks like a transformation someone forgot to finish.

Which is faster: Function with Integer or IntUnaryOperator?

For numbers IntUnaryOperator wins. The object flavour boxes an int into Integer and unboxes it back on every call, creating garbage and extra pressure on the collector. The primitive specialization works on int directly. On ten elements the difference is invisible; on a million it is not.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.