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(adouble) is autoboxed intoDouble, becauseapply()accepts an object; - inside the lambda that
Doubleis unboxed back todoubleso thatMath.round(double)can be called; - the resulting
longis boxed intoLong, because the second type parameter is declared asLong.
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.toMapandCollectors.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.
composeruns the argument function before the current one;andThenruns 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 withoutthrows, so anIOExceptioninside 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;IntUnaryOperatorcreates none. - Forgetting about null.
Functionknows nothing aboutnull: ifapply()receivesnulland the lambda calls a method on it, you get aNullPointerException. Guard it yourself or useOptional.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.
Комментарии