☕ 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;         // 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> 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

Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.