Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.
Function API — apply(), andThen(), compose(), and identity()
// ── Function basics ──────────────────────────────────────────────────
Function<String, Integer> length = String::length;
Function<String, String> upper = String::toUpperCase;
Function<Integer, String> intToStr = Object::toString;
Function<String, Boolean> isNumeric = s -> s.matches("-?\d+");
System.out.println(length.apply("hello")); // 5
System.out.println(upper.apply("hello")); // HELLO
System.out.println(intToStr.apply(42)); // "42"
System.out.println(isNumeric.apply("123")); // true
System.out.println(isNumeric.apply("abc")); // false
// ── andThen() — left-to-right composition ────────────────────────────
// f.andThen(g) = apply f first, then g
Function<String, String> upperThenTrim = upper.andThen(String::trim);
System.out.println(upperThenTrim.apply(" hello ")); // " HELLO " trimmed to "HELLO"
// Chaining multiple steps:
Function<String, String> pipeline = ((Function<String, String>) String::trim)
.andThen(String::toLowerCase)
.andThen(s -> s.replaceAll("\s+", "_"))
.andThen(s -> "prefix_" + s);
System.out.println(pipeline.apply(" Hello World ")); // "prefix_hello_world"
// ── compose() — right-to-left composition ────────────────────────────
// f.compose(g) = apply g first, then f (reverse of andThen)
Function<Integer, String> f = Object::toString; // Integer → String
Function<String, Integer> g = String::length; // String → Integer
Function<String, String> andThenComposition = g.andThen(f); // g then f: String→Integer→String
Function<String, String> composeComposition = f.compose(g); // same: g first, then f
System.out.println(andThenComposition.apply("hello")); // "5"
System.out.println(composeComposition.apply("hello")); // "5" (equivalent)
// Choosing between andThen and compose:
// Use andThen when you start from the "first" function and add subsequent steps
// Use compose when you want to "wrap" an existing function with a preprocessor:
Function<String, Integer> parseInt = Integer::parseInt;
Function<String, Integer> parseTrimmedInt = parseInt.compose(String::trim);
// parseTrimmedInt trims first, then parses — " 42 " → "42" → 42
System.out.println(parseTrimmedInt.apply(" 42 ")); // 42
// ── Function.identity() ───────────────────────────────────────────────
Function<String, String> id = Function.identity();
System.out.println(id.apply("unchanged")); // "unchanged"
// Usage in Collectors.toMap — collect to map of T → T mapping:
List<String> words = List.of("apple", "banana", "cherry");
Map<String, Integer> wordLengths = words.stream()
.collect(Collectors.toMap(
Function.identity(), // key = the word itself
String::length // value = its length
));
System.out.println(wordLengths); // {apple=5, banana=6, cherry=6}
// Usage as no-op in generic pipeline:
<T, R> R applyWithLogging(T input, Function<T, R> transform, Function<R, R> postProcess) {
R result = transform.apply(input);
System.out.println("Transformed: " + result);
return postProcess.apply(result);
}
String result = applyWithLogging("hello", String::toUpperCase, Function.identity());
// Use identity() when no post-processing is needed
System.out.println(result); // HELLO
// ── andThen with wildcard types ────────────────────────────────────────
Function<String, Integer> strToInt = String::length;
Function<Number, String> numToStr = n -> "Number: " + n;
// Integer extends Number, so ? super Integer accepts Number — type-safe:
Function<String, String> combined = strToInt.andThen(numToStr);
System.out.println(combined.apply("hello")); // "Number: 5"BiFunction, UnaryOperator, BinaryOperator, and Primitive Specializations
// ── BiFunction<T,U,R> ────────────────────────────────────────────────
BiFunction<String, String, String> concat = String::concat;
BiFunction<Integer, Integer, Integer> power = (base, exp) -> (int) Math.pow(base, exp);
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(concat.apply("Hello, ", "World!")); // Hello, World!
System.out.println(power.apply(2, 10)); // 1024
System.out.println(repeat.apply("ab", 3)); // ababab
// BiFunction.andThen() — post-process the result:
BiFunction<Integer, Integer, String> addThenStr =
((BiFunction<Integer, Integer, Integer>) Integer::sum)
.andThen(Object::toString);
System.out.println(addThenStr.apply(3, 4)); // "7"
// Map.merge() takes BiFunction:
Map<String, Integer> wordCount = new HashMap<>();
String[] words = "the quick brown fox jumps over the lazy dog the".split(" ");
for (String word : words) {
wordCount.merge(word, 1, Integer::sum); // BiFunction<Integer,Integer,Integer>
}
System.out.println(wordCount); // {the=3, quick=1, brown=1, fox=1, ...}
// ── UnaryOperator<T> — same type in and out ───────────────────────────
UnaryOperator<String> trim = String::trim;
UnaryOperator<String> upper = String::toUpperCase;
UnaryOperator<Integer> negate = n -> -n;
System.out.println(trim.apply(" hello ")); // "hello"
System.out.println(negate.apply(42)); // -42
// UnaryOperator composition (inherits andThen/compose from Function):
UnaryOperator<String> normalizeStr = trim.andThen(upper)::apply;
// Note: andThen() returns Function, not UnaryOperator — cast or use method ref
// List.replaceAll() takes UnaryOperator:
List<String> mutable = new ArrayList<>(List.of(" hello ", " world ", " java "));
mutable.replaceAll(String::trim); // modifies list in place
System.out.println(mutable); // [hello, world, java]
mutable.replaceAll(String::toUpperCase);
System.out.println(mutable); // [HELLO, WORLD, JAVA]
// ── BinaryOperator<T> ─────────────────────────────────────────────────
BinaryOperator<Integer> sum = Integer::sum;
BinaryOperator<Integer> max = Integer::max;
BinaryOperator<String> join = (a, b) -> a + ", " + b;
System.out.println(sum.apply(3, 4)); // 7
System.out.println(max.apply(10, 20)); // 20
System.out.println(join.apply("a", "b")); // "a, b"
// Stream.reduce() takes BinaryOperator:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
Optional<Integer> total = numbers.stream().reduce(BinaryOperator.minBy(Comparator.naturalOrder()));
Optional<Integer> minVal = numbers.stream().reduce(Integer::min);
int sumVal = numbers.stream().reduce(0, Integer::sum); // with identity
System.out.println(total); // Optional[1] (min)
System.out.println(sumVal); // 15
// BinaryOperator.maxBy()/minBy() — for objects:
List<String> strs = List.of("apple", "banana", "cherry", "kiwi");
Optional<String> longest = strs.stream()
.reduce(BinaryOperator.maxBy(Comparator.comparingInt(String::length)));
System.out.println(longest); // Optional[banana] or Optional[cherry] (6 chars)
// ── Primitive specializations ──────────────────────────────────────────
// ToIntFunction<T> — T → int (avoids boxing):
ToIntFunction<String> strLen = String::length; // no Integer boxing
ToLongFunction<String> strHash = s -> (long) s.hashCode();
ToDoubleFunction<String> strVal = s -> Double.parseDouble(s);
System.out.println(strLen.applyAsInt("hello")); // 5
System.out.println(strHash.applyAsLong("hello")); // some long
System.out.println(strVal.applyAsDouble("3.14")); // 3.14
// IntFunction<R> — int → R:
IntFunction<String> intToHex = Integer::toHexString;
IntFunction<int[]> intArray = int[]::new; // constructor reference
System.out.println(intToHex.apply(255)); // "ff"
System.out.println(intArray.apply(5).length); // 5
// Cross-primitive: IntToLongFunction, LongToDoubleFunction, etc.:
IntToLongFunction intToLong = n -> (long) n * n; // square as long
LongToDoubleFunction longToD = n -> n / 1_000_000.0;
System.out.println(intToLong.applyAsLong(100_000)); // 10000000000L
System.out.println(longToD.applyAsDouble(3_500_000L)); // 3.5
// In Stream pipelines — uses mapToInt/mapToLong/mapToDouble:
int[] squares = IntStream.rangeClosed(1, 5)
.map(n -> n * n) // IntUnaryOperator (same type)
.toArray();
System.out.println(Arrays.toString(squares)); // [1, 4, 9, 16, 25]
long[] longSquares = IntStream.rangeClosed(1, 5)
.asLongStream()
.map(n -> n * n) // LongUnaryOperator
.toArray();Partial Application, Currying, and Function as Pipeline Builder
// ── Partial application — fixing one argument of BiFunction ──────────
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
// Partial application: fix the second argument to 3
Function<String, String> repeatThrice = s -> repeat.apply(s, 3);
System.out.println(repeatThrice.apply("ab")); // "ababab"
System.out.println(repeatThrice.apply("xy")); // "xyxyxy"
// Generic partial application utility:
static <T, U, R> Function<U, R> partial(BiFunction<T, U, R> f, T fixedArg) {
return u -> f.apply(fixedArg, u);
}
Function<Integer, String> repeatAb = partial(repeat, "ab");
Function<Integer, String> repeatXyz = partial(repeat, "xyz");
System.out.println(repeatAb.apply(4)); // "abababab"
System.out.println(repeatXyz.apply(2)); // "xyzxyz"
// ── Currying ──────────────────────────────────────────────────────────
// Uncurried:
BiFunction<Integer, Integer, Integer> add = Integer::sum;
// Curried form:
Function<Integer, Function<Integer, Integer>> curriedAdd = a -> b -> a + b;
// Usage:
System.out.println(curriedAdd.apply(3).apply(4)); // 7
// Partial application via currying:
Function<Integer, Integer> add5 = curriedAdd.apply(5); // fix first arg to 5
System.out.println(add5.apply(10)); // 15
System.out.println(add5.apply(20)); // 25
// Curry utility:
static <T, U, R> Function<T, Function<U, R>> curry(BiFunction<T, U, R> f) {
return t -> u -> f.apply(t, u);
}
Function<String, Function<String, String>> curriedConcat = curry(String::concat);
Function<String, String> prependHello = curriedConcat.apply("Hello, ");
System.out.println(prependHello.apply("World")); // "Hello, World"
System.out.println(prependHello.apply("Java")); // "Hello, Java"
// ── Function as pipeline builder ──────────────────────────────────────
// Data transformation pipeline — each stage is a named, testable Function:
Function<String, String> normalize = s -> s.trim().toLowerCase().replaceAll("\s+", " ");
Function<String, String[]> tokenize = s -> s.split("\s+");
Function<String[], List<String>> toList = Arrays::asList;
Function<List<String>, Map<String, Long>> wordFreq = words ->
words.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// Compose the pipeline:
Function<String, Map<String, Long>> analyzeText = normalize
.andThen(tokenize)
.andThen(toList)
.andThen(wordFreq);
Map<String, Long> freq = analyzeText.apply(" the quick brown fox the fox ");
System.out.println(freq); // {the=2, quick=1, brown=1, fox=2}
// Each stage is independently testable:
System.out.println(normalize.apply(" HELLO WORLD ")); // "hello world"
System.out.println(Arrays.toString(tokenize.apply("hello world"))); // [hello, world]
// ── Memoization — cache Function results ──────────────────────────────
static <T, R> Function<T, R> memoize(Function<T, R> fn) {
Map<T, R> cache = new ConcurrentHashMap<>();
return t -> cache.computeIfAbsent(t, fn); // thread-safe: atomic per key
}
Function<Integer, Long> fib = null;
// Recursive memoized fibonacci — use array trick for self-reference:
Function<Integer, Long>[] holder = new Function[1];
holder[0] = memoize(n -> {
if (n <= 1) return (long) n;
return holder[0].apply(n - 1) + holder[0].apply(n - 2);
});
System.out.println(holder[0].apply(10)); // 55
System.out.println(holder[0].apply(40)); // 102334155 (fast — cached)
// Memoize expensive computation:
Function<String, String> expensiveHash = memoize(s -> {
// Simulate expensive hash computation:
return Integer.toHexString(s.hashCode());
});
System.out.println(expensiveHash.apply("hello")); // computed first time
System.out.println(expensiveHash.apply("hello")); // returned from cache
System.out.println(expensiveHash.apply("world")); // computed first time