☕ Java

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<T,R> has one abstract method: R apply(T t). It receives a value of type T and returns a value of type R. The types T and R can be the same (String→String), different (String→Integer), or the output can be a collection or container (String→List<String>). Any lambda or method reference whose signature matches T → R is a valid Function<T,R>: x -> x.toUpperCase() is Function<String,String>, s -> Integer.parseInt(s) is Function<String,Integer>, and Object::toString is Function<Object,String>. andThen(Function<? super R, ? extends V> after) returns a new Function<T,V> that applies this function first, then applies after to the result. f.andThen(g) represents the mathematical composition g ∘ f, applying f first. This is the natural reading order for pipelines: String → Integer → String is length.andThen(Integer::toString). The wildcard ? super R means after can accept any supertype of R (covariant); ? extends V means after can return any subtype of V (also covariant). This allows combining functions with widening types: a Function<String,Integer> can be andThen'd with a Function<Number,String> because Integer extends Number. compose(Function<? super V, ? extends T> before) is the reverse: returns a Function<V,R> that applies before first, then applies this. f.compose(g) represents g → f, applying g first. compose() is mathematically f ∘ g (f applied to the result of g). f.andThen(g) and g.compose(f) produce equivalent functions. The difference is which function you call the method on, which is a readability choice. andThen() reads left-to-right (f.andThen(g) = apply f, then g); compose() reads right-to-left (f.compose(g) = to call f, first call g). Function.identity() returns a Function that always returns its input unchanged: identity().apply(x) == x for all x. It is useful as a no-op placeholder in generic code that accepts a Function, as a collector that collects objects to themselves (Collectors.toMap(Function.identity(), mapper)), and as a neutral element for function composition. The returned function is a singleton — the same object is returned every time identity() is called. The composition methods do not evaluate the functions immediately — they create new Function objects. Calling andThen() or compose() has no effect on T or R values until apply() is called on the resulting composed function. This makes it safe and cheap to build complex composed functions before any data arrives.
Java
// ── 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;         // IntegerString
Function<String, Integer> g = String::length;            // StringInteger

Function<String, String>  andThenComposition = g.andThen(f);  // g then f: StringIntegerString
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> extends Function's concept to two input arguments with the abstract method R apply(T t, U u). BiFunction has andThen() but not compose() — compose doesn't make sense for two-argument functions because the composed function would need two outputs to feed into BiFunction's two inputs. BiFunction is used for operations that combine two values: String.format pattern applied to (template, argument), map merge operations (Map.merge()), and any computation that takes two inputs and produces one result. UnaryOperator<T> extends Function<T,T>, specializing to functions where the input and output type are the same. It overrides the apply() method signature to return T, which matches the input type. UnaryOperator.identity() exists as a more specific version of Function.identity() returning UnaryOperator<T>. UnaryOperator is used for value transformation that stays within the same type: String → String transformations, numeric computations (Integer → Integer), and list element replacement in List.replaceAll(). The static method UnaryOperator.identity() returns an operator that returns its argument unchanged. BinaryOperator<T> extends BiFunction<T,T,T>, where both inputs and the output are the same type. It is used for associative combining operations: addition (Integer::sum), maximum (Integer::max), string concatenation (String::concat). BinaryOperator.minBy(Comparator<? super T>) and BinaryOperator.maxBy(Comparator<? super T>) return operators that return the minimum or maximum of their two arguments according to the given comparator. These are used with Stream.reduce() to find extremes. The primitive function specializations cover the cartesian product of {generic, int, long, double} × {input, output}: IntFunction<R> takes int and returns R, ToIntFunction<T> takes T and returns int, IntToLongFunction takes int and returns long, and so on. Using these in primitive streams (IntStream, LongStream, DoubleStream) avoids boxing and is typically 2-5x faster for tight numeric computations. The naming convention is clear: if "Int" is a prefix to "Function", the input is int; if "ToInt" is a prefix, the output is int. Function's relationship to other interfaces: Consumer<T> is Function<T,Void> without the return; Supplier<T> is Function<Void,T> without the parameter; Predicate<T> is Function<T,Boolean> with boolean primitive return. These are distinct interfaces rather than specializations of Function because the Java type system doesn't directly express Void or boolean as generic parameters with the right semantics, and because consumers and suppliers have different semantic contracts (side effects vs pure production).
Java
// ── 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 is the technique of fixing some arguments of a multi-argument function to produce a function with fewer arguments. In Java, partial application on BiFunction is achieved by capturing one argument in a lambda that delegates to the BiFunction: given BiFunction<T,U,R> f, partial application of t produces Function<U,R> u -> f.apply(t, u). This pattern turns a two-argument function into a one-argument function configured with the fixed argument. Currying is the transformation of a multi-argument function into a chain of single-argument functions: a curried version of BiFunction<T,U,R> has type Function<T, Function<U,R>>. The curried form enables point-free programming and partial application at each argument position. In Java, curried functions are expressed as Function<T, Function<U, R>> and applied by two consecutive apply() calls: curried.apply(t).apply(u). The curried and uncurried forms have the same computational power; the choice is stylistic and depends on how the function will be partially applied. Function as a pipeline builder is the basis of the Builder pattern with functional style. Instead of a Builder class with a build() method, a series of Function transformations is composed with andThen() to produce a final transformation Function. This is especially powerful for data transformation pipelines: each stage is a named, testable Function that transforms data from one form to another, and the pipeline is assembled by composing these stages. The composed Function is then applied to each input record. The Memoization pattern uses Function to add caching to an expensive computation. A memoizing wrapper: take a Function<K,V>, return a Function<K,V> that caches results in a Map. The first call for a given key computes the result via the original function; subsequent calls return the cached result. Using ConcurrentHashMap.computeIfAbsent() makes the memoization thread-safe without explicit synchronization, because computeIfAbsent is atomic for any given key.
Java
// ── 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

Related Topics in Java 8 Features

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Consumer
Consumer<T> is a functional interface in java.util.function that represents an operation that accepts a single input argument and returns no result. It is the standard abstraction for side-effect-oriented operations — printing, logging, persisting, modifying in-place, or any action that consumes a value without producing a new one. Consumer<T> declares one abstract method, accept(T t), and one default method, andThen(Consumer<? super T> after), which composes two consumers sequentially: the second consumer receives the same input as the first, enabling chaining of multiple side effects on the same value. The java.util.function package also provides primitive specializations — IntConsumer, LongConsumer, DoubleConsumer — that avoid boxing overhead, and BiConsumer<T, U> for operations on two arguments. Consumer is used throughout the Stream API (forEach), the Collections API (Iterable.forEach, Map.forEach), and is the standard type for callback and event-handler parameters. This entry covers the full Consumer API including andThen composition, all primitive specializations and BiConsumer, the distinction between Consumer and other functional interfaces, common usage patterns in streams and collections, and how to write effective Consumer implementations.